19250 lines
717 KiB
Text
19250 lines
717 KiB
Text
= Singe Manual
|
|
Scott Duensing <scott@kangaroopunch.com>
|
|
:revnumber: 3.00
|
|
:revdate: 2026
|
|
:doctype: book
|
|
:toc: left
|
|
:toclevels: 3
|
|
:sectnums:
|
|
:sectnumlevels: 3
|
|
:source-highlighter: rouge
|
|
:icons: font
|
|
:experimental:
|
|
|
|
[preface]
|
|
== About Singe
|
|
|
|
Singe (SINGE Is Not a Game Emulator, and named after the dragon in
|
|
Dragon's Lair) is a Lua-based scripting system that allows for rapid prototyping
|
|
of new laserdisc games, or the creation of entirely new games. The language is
|
|
easy to learn, very powerful, and fast. All the features needed to develop your
|
|
own game are made available through a simple application programming interface
|
|
(API).
|
|
|
|
Singe provides numerous features to the game developer. Some of the more
|
|
interesting ones are:
|
|
|
|
* Object-oriented programming language
|
|
* Animated sprites
|
|
* TrueType font support
|
|
* 32-bit color space with transparency
|
|
* Multi-channel, overlapping, stereo sound
|
|
* Analog and digital input device support
|
|
* Wide support for video and audio formats
|
|
|
|
For players, Singe allows many unemulated and new games to be enjoyed using any
|
|
desired front end or via the included menu system.
|
|
|
|
Since the original release of Singe back in 2006, several revisions have been
|
|
released, both officially and unofficially. In 2020, Singe 2.00 was released.
|
|
This was a total rewrite from the ground up, adding numerous features while
|
|
staying compatible with existing 1.xx games. As of the writing of this manual,
|
|
the latest release is {revnumber}.
|
|
|
|
== Installation and Upgrading
|
|
|
|
=== Installing Singe
|
|
|
|
To install Singe, simply download the appropriate binary for your system. Place
|
|
it in a new folder by itself and run it. On the first run, Singe will create
|
|
all the necessary directories and support files needed. You are now ready to
|
|
install some games.
|
|
|
|
=== Upgrading Singe
|
|
|
|
To upgrade Singe to a newer version, place the new binary in the installation
|
|
folder and rename (or delete) the existing `<InstallDir>/Singe` folder. As with
|
|
the initial installation, run the new binary with no command line arguments to
|
|
generate the new support files.
|
|
|
|
WARNING: Do not run a new Singe binary against an out-of-date set of support
|
|
files. Always refresh the `Singe/` directory when upgrading.
|
|
|
|
=== Things to Know
|
|
|
|
Do not touch the `Singe/` folder. Files in this folder belong to Singe and may
|
|
be updated or deleted at any time.
|
|
|
|
== Usage
|
|
|
|
=== Installing Games
|
|
|
|
A game is either a single `.game` file or a directory. Copy it into the same
|
|
folder where you placed the Singe binary. The included menu system will
|
|
automatically detect it and add it to the menu.
|
|
|
|
[#bundledmenu]
|
|
=== The Bundled Menu
|
|
|
|
`Menu.sh` (or `Menu.bat`) starts `Singe/Menu.singe`, the bundled menu: an
|
|
RmlUi document, `Singe/Menu.rml` styled by `Singe/menu.rcss`, drawn over the
|
|
engine's own disc (see <<gui,GUI>>). It reads the `games.dat` of every game
|
|
directory and `.game` file beside it and lists every game it finds, sorted by
|
|
title, down the left of the screen. Beside the list are the selected game's
|
|
cabinet art, its marquee and its attract clip, with the year, genre,
|
|
platform, developer, publisher, port credits and description underneath and
|
|
a line of key hints along the bottom.
|
|
|
|
Up and down move the selection one game at a time and left and right move it
|
|
a page at a time; both wrap from either end of the list to the other. Start
|
|
or any action button launches the selected game, and the key mapped to
|
|
`INPUT_SERVICE` (the `9` key by default) opens the service tools (see
|
|
<<servicetools,The Service Tools>>). With a mouse, a
|
|
click on a row selects that game and a second click on the same row, or a
|
|
click on the Start button in the footer, launches it; the mouse wheel
|
|
scrolls a description too long for its panel. The menu keeps its selection
|
|
in `menu.dat` in its data directory, so it comes back to the same game when
|
|
one quits or when Singe is next started. That file is a Lua fragment the menu
|
|
reads at startup and rewrites at shutdown: `GAME_SELECTED` is the game it was
|
|
showing, `SHOW_INTRO` is false when it is returning from a game so the intro
|
|
is not sat through twice, and `MENU_OVERLAY` chooses a renderer (see
|
|
<<menurenderers,The two renderers>>).
|
|
|
|
[#menurenderers]
|
|
==== The two renderers
|
|
|
|
A GUI document is drawn through the GPU device the 3D scene uses, so on a
|
|
machine without one there is no document to build. The menu handles that
|
|
itself: `Singe/Menu.singe` asks <<singehasgpu,singeHasGpu>> at startup and
|
|
loads one of two renderers.
|
|
|
|
`Singe/MenuDocument.singe`::
|
|
The RmlUi document described above, for a machine with a GPU device. It also
|
|
draws the intro and the grid behind the menu as one continuous 3D scene, so
|
|
the disc is not used for either.
|
|
|
|
`Singe/MenuOverlay.singe`::
|
|
Sprites and `fontPrint` straight into the overlay, the look Singe 2 had. It
|
|
needs nothing beyond the overlay every game already draws into.
|
|
|
|
Both draw the same menu, take the same keys and run the same service tools;
|
|
`Singe/Menu.singe` holds everything that is not drawing -- the game list, the
|
|
selection, launching a game -- so there is one menu rather than two. The
|
|
overlay renderer shows one game at a time instead of a list, since it has no
|
|
panel to scroll, and it clips a description too long for its column rather
|
|
than scrolling it.
|
|
|
|
To see the overlay renderer on a machine that does have a GPU, put
|
|
`MENU_OVERLAY = true` in `menu.dat` in the menu's data directory; the menu
|
|
writes the line back out, so it survives. That is how the overlay path is
|
|
tested, and how a cabinet builder who prefers the old look gets it.
|
|
|
|
[#onlineservice]
|
|
==== Online: accounts, games and scores
|
|
|
|
The bundled menu can sign the cabinet in to a master service (by default
|
|
`master.singeengine.com`), which does three things: it holds the account, it
|
|
offers a catalogue of games to download and update, and it keeps high score
|
|
boards.
|
|
|
|
Two service tools cover it. **Online Account** signs in, creates an account,
|
|
recovers a password, and sets the server address -- it is where an operator
|
|
running their own service points the cabinet. A new account is not usable until
|
|
the emailed link is opened. **Get Games** lists everything the
|
|
service offers, marks what is installed here, and downloads, updates or removes
|
|
it. A list longer than the screen scrolls with the selection rather than running
|
|
off the bottom, with `...` showing there is more either way. A game the service has withdrawn keeps working and is shown as "installed,
|
|
no longer offered": all that stops is being given an update, because a player
|
|
who has a game has it. A download is checked against the digest the catalogue published before it
|
|
replaces anything, so a transfer that arrives wrong fails rather than installing
|
|
a game that will not run.
|
|
|
|
The account belongs to the *machine*, not to a game: the token is kept in
|
|
`master.dat` in the data root, so a game posting a score uses the same sign-in
|
|
the menu made. See <<onlinescores,Online high scores>> for the game's side, and
|
|
`Singe/Net.singe` for the asynchronous HTTP client underneath -- nothing here
|
|
blocks a frame, because Lua in Singe is cooperative and a large download would
|
|
otherwise look like a hang.
|
|
|
|
[#servicetools]
|
|
==== The Service Tools
|
|
|
|
The key mapped to `INPUT_SERVICE` opens a list of tools over the game list.
|
|
Up and down choose one and button 1 opens it; button 2 backs out of a tool to
|
|
the list, and the service key again leaves the tools altogether. Everything
|
|
lives in `Singe/Tools.singe`, which the engine extracts beside `Menu.singe`.
|
|
Nothing there draws: a tool builds a list of rows and hands it to the menu,
|
|
which is why the same ten tools run on both renderers (see
|
|
<<menurenderers,The two renderers>>). Nothing here needs a game to be running,
|
|
and nothing changes a game's own files.
|
|
|
|
Audio Delay::
|
|
The downstream audio delay, measured against a click and a flash. See
|
|
<<menucalibration,Calibrating from the menu>> for what it is doing and why it
|
|
works.
|
|
|
|
Input Test::
|
|
Every controller, mouse and gun the engine can see, live: the last switch the
|
|
game would have received and how many have arrived, each pad's name with its
|
|
six axes and the buttons held down, and each mouse's name and position. This
|
|
is the screen that answers "the button does nothing" -- either the switch
|
|
arrives and the game ignores it, or it never arrives at all.
|
|
|
|
System Information::
|
|
The engine's version, the operating system, the processor and memory, the
|
|
renderer and the 3D device, which video decoders are in play, the audio
|
|
formats built in, the SoundFont, the MIDI state, the window, canvas and
|
|
overlay sizes, and the data directory. A photograph of this page answers most
|
|
of a bug report.
|
|
|
|
Light Gun::
|
|
Targets in the centre and at each corner of the overlay, over whatever the
|
|
disc is showing. Button 1 marks where the gun actually pointed, keeping the
|
|
last twelve shots; start clears them. A gun that is square on the picture puts
|
|
its crosses on the targets, and one that is not shows which way it is out.
|
|
|
|
Sound Test::
|
|
Button 1 plays the click through the left speaker, then the right, then both,
|
|
which is the whole of a crossed-wires check. It also lists the disc's own
|
|
audio tracks, and left and right switch between them.
|
|
|
|
Display::
|
|
The picture's size, position and rotation, changed live with the arrows,
|
|
buttons 3 and 4, and coin 1, over a test pattern: a 16 by 12 grid to square
|
|
the picture up with, an eight step greyscale wedge to set brightness by, and
|
|
two frames one pixel apart at the very edge -- a monitor that overscans eats
|
|
the outer one first. Button 1 keeps the values for this machine, in the same
|
|
`machine.cfg` the audio delay uses, so they apply to every game rather than
|
|
being set once per game. Start puts everything back.
|
|
|
|
Disc Test::
|
|
Twelve seeks spread over the whole disc, each timed. A rip whose keyframes are
|
|
far apart seeks slowly, and the only symptom in a game is that everything
|
|
feels heavy; this measures it directly and reports the worst of the twelve.
|
|
A seek that has not landed in three seconds is counted as that.
|
|
|
|
Saved Data::
|
|
What each game has kept, found by looking through the data root rather than by
|
|
being told, with the size of each. Button 3 -- not button 1, which is "choose"
|
|
everywhere else -- throws away the selected game's save. Clearing a high score
|
|
table without deleting the game is the usual reason.
|
|
|
|
Online Account::
|
|
Signing this cabinet in to the master service, creating an account, recovering a
|
|
password, and setting the server address. See
|
|
<<onlineservice,Online: accounts, games and scores>>.
|
|
|
|
Get Games::
|
|
The catalogue: what the service offers, what is installed here, and downloading,
|
|
updating or removing it.
|
|
|
|
MIDI Ports::
|
|
The input and output ports this machine has, which of them Singe has opened,
|
|
and the last few messages that arrived. Button 1 sends a note, button 3 looks
|
|
for ports again after something has been plugged in. Nothing is opened until
|
|
this tool asks, so a cabinet that never touches MIDI never pays for it.
|
|
|
|
Frame Statistics::
|
|
Turns the developer's overlay on and off. It stays on into the game, in the
|
|
top left corner. See <<statsenable,statsEnable>>.
|
|
|
|
[[controls]]
|
|
=== Customizing the Controls
|
|
|
|
By default, Singe maps controllers as if they were Xbox gamepads. If your
|
|
controller is not recognized or you wish to change the default mappings as they
|
|
appear to Singe, you can use the free SDL2 Gamepad Tool from
|
|
http://generalarcade.com/gamepadtool/.
|
|
|
|
[[devices]]
|
|
==== Every Device Is a Gamepad
|
|
|
|
Singe has one family of controller, `GAMEPAD_0` through `GAMEPAD_3`, and every
|
|
device it opens is in it. SDL calls a device a gamepad only when it has a
|
|
mapping for it, and an arcade encoder board, a spinner, a wheel or a flight
|
|
stick often has none. Rather than carry a second kind of device with its own
|
|
names, Singe writes a mapping for such a device itself and then opens it as an
|
|
ordinary gamepad, so it reaches `controls.cfg`, `controllerGetAxis`,
|
|
`controllerGetButton` and every `INPUT_*` switch exactly as a recognised pad
|
|
does.
|
|
|
|
The mapping Singe writes is a straight-through one: the device's axes in order
|
|
become the left stick, the right stick and then the two triggers; its buttons
|
|
in order become `BUTTON_A`, `BUTTON_B`, `BUTTON_X`, `BUTTON_Y`, `BUTTON_BACK`,
|
|
`BUTTON_GUIDE`, `BUTTON_START`, `BUTTON_LEFT_STICK`, `BUTTON_RIGHT_STICK`,
|
|
`BUTTON_LEFT_BUMPER` and `BUTTON_RIGHT_BUMPER`; and its first hat becomes the
|
|
D-pad. A device with no hat spends its next four buttons on the D-pad instead.
|
|
**A device with more controls than the gamepad model can name loses the
|
|
extras** -- more than six axes, more than fifteen buttons, or a second hat --
|
|
and a device whose buttons are not wired in that order will have them in the
|
|
wrong places. That is the price of one family instead of two, and it is
|
|
straightforward to correct: `--program` traces the mapping it wrote, in SDL's
|
|
own format, so you can paste it into a `gamecontrollerdb.txt`, rearrange it and
|
|
have it used instead.
|
|
|
|
`--mapjoysticks=false` turns the writing off, in which case a device SDL does
|
|
not recognise is not opened at all, exactly as in earlier releases.
|
|
|
|
[[gamecontrollerdb]]
|
|
==== gamecontrollerdb.txt
|
|
|
|
SDL ships with a large database of controller mappings, and the community keeps
|
|
a larger one -- `gamecontrollerdb.txt`, from
|
|
https://github.com/mdqinc/SDL_GameControllerDB. Singe looks for that file in
|
|
the same four places it looks for `controls.cfg`:
|
|
|
|
. the directory Singe was started in;
|
|
. the directory above the game's data directory;
|
|
. the game's data directory;
|
|
. the game's main script directory.
|
|
|
|
Every file found is loaded, in that order, so one beside a game wins over a
|
|
machine-wide one, and both win over SDL's own database. Only a device that
|
|
none of them names gets the mapping Singe writes for it, so the order of
|
|
precedence is: SDL's built-in database, then your `gamecontrollerdb.txt`, then
|
|
the synthetic mapping. `--program` reports how many mappings each file added.
|
|
|
|
A line of that file looks like this, and is exactly what the trace prints for a
|
|
device Singe mapped itself:
|
|
|
|
----
|
|
ff00d87a475300004900000000007603,Arcade Encoder,platform:Linux,a:b0,b:b1,x:b2,y:b3,back:b4,guide:b5,start:b6,leftx:a0,lefty:a1,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
|
|
----
|
|
|
|
In addition to configuring your controller, you can remap all the input options
|
|
for Singe as a whole or for any individual game by using `controls.cfg` files.
|
|
When determining how inputs are mapped, Singe reads configuration settings in
|
|
the following order:
|
|
|
|
. The defaults from inside Singe.
|
|
. `controls.cfg` from the directory Singe was started in (the `Singe` directory holds a `controls.cfg.example` template you can copy).
|
|
. `controls.cfg` from the directory above the game's data directory.
|
|
. `controls.cfg` from the game's data directory.
|
|
. `controls.cfg` from the game's main script directory.
|
|
|
|
You do not have to specify every option in a custom `controls.cfg` file. Feel
|
|
free to just provide the entries you want changed.
|
|
|
|
After installing Singe, you can find a sample input configuration file at
|
|
`<InstallDir>/Singe/controls.cfg.example`. To see the available configuration
|
|
options available to be used in this file, read through
|
|
`<InstallDir>/Singe/Framework.singe`.
|
|
|
|
`--keymapfile` names one file to read instead of making that search, which is
|
|
useful to a front end that wants no doubt about which mappings a game got.
|
|
|
|
[[deadzones]]
|
|
==== Dead Zones and Triggers
|
|
|
|
`DEAD_ZONE` in `controls.cfg` is how far an axis must move before it counts as
|
|
a pressed switch, in the raw units SDL reports: `32767` at full deflection, so
|
|
the default of `15000` is a little under half way. It covers every axis of
|
|
every pad, the two analogue triggers included, and a script reads it as
|
|
`SINGE_DEAD_ZONE`.
|
|
|
|
Two settings narrow that down.
|
|
|
|
`DEAD_ZONES` is a list of `{ axis, dead zone }` pairs for one axis at a time,
|
|
in those same raw units, for a stiff stick or a worn trigger that should not
|
|
move everything else:
|
|
|
|
[source,lua]
|
|
----
|
|
DEAD_ZONES = {
|
|
{ GAMEPAD_0.AXIS_LEFT_X, 8000 },
|
|
{ GAMEPAD_1.AXIS_LEFT_TRIGGER, 30000 },
|
|
}
|
|
----
|
|
|
|
The axis may be written as the axis itself or as either of its two directions
|
|
(`GAMEPAD_0.AXIS_LEFT_X`, `AXIS_LEFT_X_L` and `AXIS_LEFT_X_R` all name the same
|
|
axis). Anything not listed keeps `DEAD_ZONE`.
|
|
|
|
`TRIGGER_THRESHOLD` is how far an analogue trigger must travel before it counts
|
|
as a button, as a *per cent of full travel*, which is the unit Hypseus's
|
|
`-trigger-threshold` uses and whose default there is `99.5`. It applies to both
|
|
triggers of all four pads. Singe's default is `0`, meaning the triggers stay on
|
|
`DEAD_ZONE`, so nothing changes for anyone who does not ask; a script reads the
|
|
result in raw units as `SINGE_TRIGGER_THRESHOLD`. The two units convert
|
|
directly: a per cent is `32767 / 100` raw units, so Singe's `DEAD_ZONE` of
|
|
`15000` is `45.8` per cent and Hypseus's `99.5` per cent is `32603` raw units.
|
|
`--trigger_threshold` sets the same number from the command line and wins over
|
|
`controls.cfg`; `DEAD_ZONES` wins over both, being the most specific.
|
|
|
|
[[joymouse]]
|
|
==== Driving the Mouse With a Stick
|
|
|
|
A light gun game on a cabinet with a stick and no mouse is unplayable unless
|
|
the stick can move the cursor. `--joymouse` makes it do so: the **left stick of
|
|
gamepad slot 0** moves the pointer the game sees, through the same
|
|
`onMouseMoved` a real mouse reaches, and the cursor is held inside the picture.
|
|
`--js_range` is the speed, `1` to `20` with `5` the default, measured in video
|
|
pixels a frame at full deflection, so `5` crosses a 720 pixel picture in about
|
|
two seconds. The axis obeys its own dead zone, so a drifting stick does not
|
|
creep.
|
|
|
|
It is off by default, which is where Singe differs from Hypseus: the shipped
|
|
`controls.cfg` already binds that stick to `INPUT_UP`, `INPUT_DOWN`,
|
|
`INPUT_LEFT` and `INPUT_RIGHT`, and having it drive the cursor as well without
|
|
being asked would surprise every existing game. `--joymouse=false` is
|
|
Hypseus's `-nojoymouse`, and a script turns it on and off with `joyMouseEnable`
|
|
and reads it with `joyMouseIsEnabled`. A reload puts it back where the command
|
|
line left it.
|
|
|
|
[[mousedevices]]
|
|
==== Mice, Guns, and Who Chooses
|
|
|
|
`mouseSetMode(MOUSE_MANY)` is a game's decision to tell up to four mice apart,
|
|
which a two gun cabinet needs and which a game that never makes the call cannot
|
|
be given. `--manymouse` is the user's decision and outranks it: the mode is on
|
|
from the start and `mouseSetMode(MOUSE_SINGLE)` no longer switches it off.
|
|
|
|
`--absolutes_only` keeps only the devices that report an absolute position,
|
|
which is what a real light gun does and an ordinary mouse does not, so a
|
|
cabinet with two guns and a desk mouse plugged in is not confused by the mouse.
|
|
Be aware of how it decides: **ManyMouse, the backend Singe uses to tell mice
|
|
apart, offers no way to ask a device what kind it is**, so a device counts as
|
|
absolute once it has reported an absolute position and not before. A gun does
|
|
that with its first movement; until then it is silent, and a relative-only
|
|
device stays silent for good. The option is therefore a filter by observed
|
|
behaviour rather than by device class, and on a machine where no device ever
|
|
reports an absolute position it silences every mouse.
|
|
|
|
`SWITCH_MOUSE_DISCONNECT` is a switch like any other, except that no key or
|
|
button produces it: the engine raises it when a mouse or a light gun is
|
|
unplugged, once as a press and once as a release, so a gun game can say so on
|
|
screen instead of appearing to have died. It is Hypseus's switch of the same
|
|
name. `controls.cfg` leaves it bound to nothing; bind a key to it as well if
|
|
you want to see what a game does without pulling a cable.
|
|
|
|
[[hotkeys]]
|
|
==== Hotkeys
|
|
|
|
`Alt-Enter` switches between full screen and a window while the game runs, as
|
|
it does in Hypseus. The game never sees the key. A window that started in
|
|
exclusive full screen (`--fullscreen`) returns to it in the display mode it was
|
|
given; one that started windowed gets a borderless window covering the desktop.
|
|
Nothing else has to change with it: the renderer presents a logical size in
|
|
video coordinates and scales that to whatever the window now is, the video
|
|
rectangle is built in those same coordinates, and the rotation target is the
|
|
size of the picture rather than of the window, so the picture, the overlay, the
|
|
GUIs and the mouse mapping all follow by themselves.
|
|
|
|
The other switches a user can press -- quit, pause, screenshot, the console and
|
|
the mouse capture toggle -- are ordinary `controls.cfg` mappings and are
|
|
described with the `INPUT_*` names in the sample file.
|
|
|
|
[[settings]]
|
|
=== The Settings File
|
|
|
|
Everything on the command line can also be written down once, in a
|
|
`settings.cfg` file, so a cabinet does not need a launcher script carrying the
|
|
same dozen options for every game. It is Lua, like `controls.cfg`, and Singe
|
|
looks for it in exactly the same four places, in this order:
|
|
|
|
. `settings.cfg` from the directory Singe was started in (the `Singe` directory holds a `settings.cfg.example` template you can copy).
|
|
. `settings.cfg` from the directory above the game's data directory.
|
|
. `settings.cfg` from the game's data directory.
|
|
. `settings.cfg` from the game's main script directory.
|
|
|
|
Each place is read once, and a later file wins key by key, so a machine-wide
|
|
file can set the display and the volumes while a file beside one game turns off
|
|
its crosshair. You do not have to write every key in a file; write the ones you
|
|
want changed.
|
|
|
|
Every key is the long name of a command line option with the leading dashes
|
|
removed, so there is no second vocabulary to learn:
|
|
|
|
[source,lua]
|
|
----
|
|
-- The cabinet's own settings.cfg
|
|
screen = 2 -- the second monitor
|
|
fullscreen = true
|
|
volume_vldp = 80
|
|
haptic = 0 -- this cabinet has no rumble
|
|
idleexit = 900 -- hand the screen back after fifteen quiet minutes
|
|
startsilent = true
|
|
----
|
|
|
|
An option that takes no value on the command line takes `true` or `false` here
|
|
(`yes`, `no`, `on`, `off`, `1` and `0` are accepted too). Setting one to `false`
|
|
is the same as leaving it out. An option that takes a value takes the same value
|
|
it would on the command line, as a number or a string.
|
|
|
|
One option, `--deterministic`, takes a value or leaves it out, and its key does
|
|
the same: `deterministic = true` uses the default step, `deterministic = 20`
|
|
names one, and `deterministic = false` leaves the option off.
|
|
|
|
==== What wins
|
|
|
|
. The built in default.
|
|
. The settings file.
|
|
. The game's `games.dat` entry, for the handful of settings it carries (see <<gamesdat,games.dat>>).
|
|
. `machine.cfg`, for the audio delay and the picture geometry the service tools
|
|
save (see <<servicetools,The Service Tools>>).
|
|
. The command line, which always wins.
|
|
|
|
A settings file is a set of defaults, in other words, not something you typed:
|
|
if a `games.dat` entry gives a game a resolution or a Sinden border, that entry
|
|
still describes the game better than a machine-wide file does. Anything actually
|
|
typed on the command line beats both. `machine.cfg` sits just under the command
|
|
line because somebody adjusted the picture while looking at this monitor, which
|
|
is better evidence than any file written elsewhere; the audio delay is the
|
|
exception to the ordering entirely, being added to the game's delay rather than
|
|
replacing it.
|
|
|
|
Options that name the game (`--framefile`, `--disc`, `--entry`), name a
|
|
directory (`--gamedir`, `--datadir`), or have to act before the file could be
|
|
found (`--help`, `--apiversion`, `--noconsole`, `--pack`, `--patch`,
|
|
`--unpack`) cannot be set in a settings file. A key that is not an option, one
|
|
of those, or a value the option will not take is an error naming the file and
|
|
the key, so a typo is never silently ignored:
|
|
|
|
----
|
|
settings.cfg: volume_vldpp is not an option.
|
|
settings.cfg: datadir cannot be set here; it belongs on the command line.
|
|
settings.cfg: Display scale must be between 25 and 100 percent.
|
|
----
|
|
|
|
With `--program` or `--trace`, the header at the top of `trace.txt` lists every
|
|
setting that was applied, so there is never a question about which file a
|
|
running game was given.
|
|
|
|
[[commandlineoptions]]
|
|
=== Command Line Options
|
|
|
|
----
|
|
Singe [OPTIONS] gameName
|
|
----
|
|
|
|
The game name is the only required argument. It may be a `.singe` script, a
|
|
directory containing a script of the same name (`ActionMax` finds
|
|
`ActionMax/ActionMax.singe`), or a packed game (`DLe.game` runs the first
|
|
entry of the `games.dat` inside it, `--entry=N` another). A script or directory
|
|
launched this way also runs with its `games.dat` entry when one names it (see
|
|
<<gamesdat,games.dat>>), so a game plays the same from the command line as from
|
|
the menu. For a laserdisc game (`--disc`) with no
|
|
`--framefile`, Singe looks for a video next to the script with the same base
|
|
name and any extension FFmpeg can demux, then for a `.txt` framefile.
|
|
|
|
[cols="1,2",options="header"]
|
|
|===
|
|
| Option | Purpose
|
|
| `-A`, `--audiodelay=MS` | Compensate for audio that is heard `MS` milliseconds later than the engine can measure (negative when it is heard early), `-1000` to `1000`. See <<audiosync,Audio Sync>>.
|
|
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
|
|
| `-B`, `--bezel=FILENAME` | Draw cabinet artwork around the picture. The file is looked for in the `bezels` folder of the game directory (a packed game may carry its own) and then in the `bezels` folder of the data directory; `--bezeldir` names a different folder. Any format SDL_image reads will do, and transparency is kept. A sidecar beside it says where the picture goes inside it; with none, the picture keeps the whole window and `--shiftx`, `--shifty` and `--scalefactor` place it. Artwork that cannot be found ends the program. See <<bezels,Bezels>>.
|
|
| `-b`, `--scalefactor=PERCENT` | Shrink the picture about its centre to `PERCENT` (`25` to `100`) of the window for displays that lose their edges to overscan. Mouse and light gun positions follow the shrunken picture. A script may change it while the game runs with `vldpSetScale`. Combines with `--shiftx`, `--shifty`, `--rotate` and `--sindengun`; see <<videorect,The Video Rectangle>>.
|
|
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
|
|
| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc.
|
|
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
|
|
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, saves, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it (for a packed game, one named for the database and then for the script's directory inside it). Defaults to `data` in the game directory. Singe 2.x wrote beside the game instead; game directories may be read only, so nothing is written there any more.
|
|
| `-E`, `--entry=N` | Run the Nth entry of the `games.dat` inside a `.game` file (default 1). See <<singlefile,Single-File Games>>.
|
|
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
|
|
| `-F`, `--bezelflip` | Draw the bezel artwork in front of the picture instead of behind it, which is what Hypseus does with its own bezels; the artwork's transparency is then the hole the game shows through. Without it the artwork is behind everything and the picture covers its cutout. A script chooses whether the overlay and the GUIs stay above the artwork with `setOverlayOnTop`. See <<bezels,Bezels>>.
|
|
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
|
|
| `-G`, `--gamedir=PATHNAME` | The directory holding the games, the packed `.game` files and the `Singe` support folder, when the executable lives somewhere else. It becomes the working directory, so the game name, a relative `--datadir` and the packer's names all count from it; absolute paths work as they are. Defaults to the current directory.
|
|
| `-g`, `--sindengun='PARAMS'` | Draw the border a Sinden light gun tracks and map mouse and light gun positions into the picture inside it: the black ring, when one is given, runs along the edge of the video, the white ring sits inside it, and the game is drawn in the middle. `PARAMS` is one quoted list of numbers: `WW` (white border width in video pixels); `WW WB` (white width, then a black border outside it); `RW GW BW WW` (a custom border color and width); `RW GW BW WW WB` (the same plus a black width); or `RW GW BW WW RB GB BB WB` (custom colors and widths for both). A border that leaves no room for the game (twice the white and black widths together reaching the smaller side of the video), or a negative width, ends the program with a message. The rings are drawn around whatever `--shiftx`, `--shifty` and `--scalefactor` left, so a border stays a border however the picture is placed; see <<videorect,The Video Rectangle>>.
|
|
| `-H`, `--softwarevideo` | Decode video in software even when the platform offers a hardware decoder (VA-API or VDPAU on Linux, the V4L2 decoder on the Pi, D3D11VA on Windows, VideoToolbox on macOS). Use it to rule the hardware path in or out when a video misbehaves; the program trace says which decoder is in use.
|
|
| `-h`, `--help` | Show the option summary and exit.
|
|
| `-I`, `--bezeldir=PATHNAME` | The folder holding `--bezel`'s artwork, in place of `bezels`. Relative to the game directory, or absolute. Only that folder is searched. See <<bezels,Bezels>>.
|
|
| `-k`, `--nologos` | Skip the splash screens.
|
|
| `-l`, `--volume_vldp=PERCENT` | Laserdisc volume, `0` to `100`.
|
|
| `-m`, `--nomouse` | Disable mouse and lightgun input entirely.
|
|
| `-n`, `--nocrosshair` | Ask the game not to draw its own crosshair. Exposed to scripts as `singeWantsCrosshairs()`.
|
|
| `-o`, `--audio=TRACK` | Default audio track (zero based) for videos with several, such as multi-language releases.
|
|
| `-P`, `--pack=DIRECTORY` | Pack the game in `DIRECTORY` into the database named after the options, then exit. See <<singlefile,Single-File Games>>.
|
|
| `-p`, `--program` | Trace engine activity to the console and to `trace.txt` in the data directory. The file opens with a header describing the machine; see <<traceheader,The Trace Header>>.
|
|
| `-S`, `--sindenedge=WHERE` | Where the Sinden border sits: `video` draws it around the picture, `window` at the edge of the screen. Without the option a bezel decides: artwork around the picture puts the border at the window's edge, because the gun's camera sees the whole screen, and with no artwork it goes around the picture as it always has. At the window's edge the two rings are drawn as rings, last of all, so the artwork between them and the picture stays visible. See <<bezels,Bezels>>.
|
|
| `-s`, `--nosound` | Mute all audio.
|
|
| `-T`, `--patch=GAME.game` | Replace files in the packed game from the directory or patch database named after the options, then exit.
|
|
| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`. The file opens with a header describing the machine; see <<traceheader,The Trace Header>>.
|
|
| `-r`, `--rotate=DEGREES` | Turn the whole presentation clockwise by `0`, `90`, `180` or `270` degrees, for a cabinet whose panel is mounted on its side. The video, the overlay, the GUIs, the 3D scene and the particles all turn together, a quarter turn swaps the shape of the picture so a portrait window is filled, and mouse and light gun positions are turned back so a click still lands where it is drawn. Only the four right angles are accepted. A script may change it with `vldpSetRotate`.
|
|
| `-R`, `--reload` | Watch the game's loose script files and run the game again from scratch when one changes; `F5` does the same on demand. A script that fails to compile stays watched, so saving the fix reloads it. For working on a game; see Reloading While You Work.
|
|
| `-U`, `--unpack=GAME.game` | Write the packed game's files into the directory named after the options, then exit. A database whose stored names would land outside that directory is refused.
|
|
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
|
|
| `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script.
|
|
| `-w`, `--fullscreen_window` | Borderless window covering the desktop.
|
|
| `-X`, `--shiftx=PERCENT` | Move the picture horizontally inside the room `--scalefactor` left it, `-100` (hard against the left) to `100` (hard against the right), `0` centred. As in Hypseus, the shift is a percentage of that room and nothing more, so at `--scalefactor=100` there is no room and the shift does nothing: reduce the scale first. See <<videorect,The Video Rectangle>>.
|
|
| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted.
|
|
| `-Y`, `--shifty=PERCENT` | Move the picture vertically inside the room `--scalefactor` left it, `-100` (hard against the top) to `100` (hard against the bottom), `0` centred. It does nothing at full scale, for the same reason. See <<videorect,The Video Rectangle>>.
|
|
| `-y`, `--yresolution=VALUE` | Window height.
|
|
| `-z`, `--noconsole` | Print nothing (and open no console window on Windows).
|
|
| `--absolutes_only` | Keep only the mice that report an absolute position, which is what a real light gun does and an ordinary mouse does not. ManyMouse cannot be asked what a device is, so a device counts as absolute once it has reported an absolute position and not before; see <<mousedevices,Mice, Guns, and Who Chooses>>. Hypseus writes it `-absolutes-only`; the name here uses an underscore because a settings file key has to be a Lua name. Default: off.
|
|
| `--altaudio=SUFFIX` | Play `<base><SUFFIX>.ogg` beside the disc's video instead of the audio inside it, for a release whose other languages ship as separate files: `--altaudio=-es` next to `lair.m2v` plays `lair-es.ogg`. Every segment of a framefile is switched together. A file that is not there leaves the game's own audio playing and prints a warning. The `AUDIO_SUFFIX` key in `games.dat` does the same for one game, and a script changes it while running with `discAudioSuffix`. Default: none.
|
|
| `--apiversion` | Print one machine readable line describing this build to standard output and exit, for front ends. See <<apiversion,The Version Line>>. Nothing else is printed.
|
|
| `--deinterlace=MODE` | What an interlaced picture gets: `auto` (the default) deinterlaces only the frames the file marks interlaced, `on` deinterlaces every frame for a file whose flags are wrong, and `off` never touches the picture. A laserdisc held interlaced fields and a rip that kept them combs on a progressive display; a rip that was deinterlaced when it was made needs nothing here. One picture comes out for each one that goes in, so frame numbers never move. Default: `auto`.
|
|
| `--deterministic[=MS]` | For testing only: ignore real time and run the whole engine on a virtual clock that moves `MS` milliseconds every frame, so the same frame number always means the same moment. The disc steps one video frame a frame with it, and the random generators are seeded from the same number, so a run repeats to the pixel. The value is optional and is both the step and the seed, `1` to `1000`. Default when given without one: `15`, the frame time the engine's own rate implies. Audio and pacing are meaningless in this mode; see <<deterministic,Deterministic Test Mode>>. Default: off.
|
|
| `--fvalue=NUMBER` | One number handed from the launcher to the game, which reads it with `getFValue()`. Singe does nothing with it. `0` to `100000`, kept to three decimals as Hypseus keeps it. Default: `0`.
|
|
| `--gamepad_reorder=DIGITS` | Which physical pad fills which gamepad slot, as enumeration positions counting from `0`, one for each slot in turn: `--gamepad_reorder=10` makes the second pad found player one and the first player two. Written as bare digits (`3210`) or separated by commas or spaces, as Hypseus writes it; a repeated position is refused. Positions not named fill the slots that are left, in the order SDL found them, and a position with no pad behind it leaves its slot empty. Default: SDL's own order.
|
|
| `--haptic=STEP` | The strongest rumble step a game may use, `0` to `4`. `0` turns rumble off altogether, and a lower number quietens a game that asks for more: `controllerDoRumble` never rumbles harder than this. Default: `4`, which is every step a game asks for.
|
|
| `--idleexit=SECONDS` | Quit after `SECONDS` with no input of any kind -- no key, no button, no pad axis, no mouse. For an attract cabinet that should hand the screen back. `0` never quits. `0` to `86400`. Default: `0`.
|
|
| `--joymouse=BOOLEAN` | Let the left stick of gamepad slot `0` drive the mouse cursor, for a light gun game on a cabinet with no mouse. `--joymouse=false` is Hypseus's `-nojoymouse`, and a script sets it with `joyMouseEnable()`. See <<joymouse,Driving the Mouse With a Stick>>. Default: `false`, where Hypseus's default is on -- the shipped `controls.cfg` already binds that stick to the four directions.
|
|
| `--js_range=SPEED` | How fast that stick drives it, `1` to `20`, in video pixels a frame at full deflection. Default: `5`, as in Hypseus.
|
|
| `--keymapfile=FILENAME` | Read the control mappings from this file instead of searching the four places for `controls.cfg`, so a front end can be certain which mappings a game got. The built in defaults still apply underneath it, so a partial file still works, and a file that is not there ends the program. Default: the four place search, described in <<controls,Customizing the Controls>>.
|
|
| `--linearscale=BOOLEAN` | Smooth the overlay as the window scales it (`true`) or take the nearest pixel (`false`, which keeps pixel art crisp). Takes `true`, `false`, `yes`, `no`, `on`, `off`, `1` or `0`. A script changes it while running with `setOverlayLinearScale`. Default: `true`, which is what every Singe release has drawn; Hypseus's own default is the opposite.
|
|
| `--manymouse` | Tell up to four mice apart whatever the game asks for, which a two gun cabinet needs from a game that never calls `mouseSetMode(MOUSE_MANY)` itself. The user's choice outranks the game's: `mouseSetMode(MOUSE_SINGLE)` no longer switches it off. Default: off, and the game decides.
|
|
| `--mapjoysticks=BOOLEAN` | Write a gamepad mapping for a device SDL does not recognise, so an arcade encoder, a spinner, a wheel or a flight stick is opened as `GAMEPAD_0` to `GAMEPAD_3` like anything else. `false` leaves such a device unopened, as earlier releases did. See <<devices,Every Device Is a Gamepad>>. Default: `true`.
|
|
| `--monochrome` | Start with the disc picture in grey. The overlay, the GUIs and the 3D scene keep their colour; a script does the same with `vldpSetMonochrome`, and undoes it. Default: off.
|
|
| `--nogamepad` | Ignore every gamepad, the counterpart of `--nomouse`. None is opened and no pad event reaches the game, so a stuck arcade encoder cannot press anything. Default: off.
|
|
| `--screen=N` | Open the window on display `N`, counting from `1` as Hypseus counts them. A number larger than the number of displays lists the displays that are there and exits. Default: the primary display.
|
|
| `--soundfont=FILE` | The SoundFont (`.sf2`) MIDI files are synthesised with. Singe ships none, because a good one is tens of megabytes for a format almost no game uses. Without this option it looks for `Singe/soundfont.sf2` -- so a packed game can carry its own -- and then in the places a distribution installs one. The `SoundFont:` line of the trace header says which was used, or that none was found. See <<midi,MIDI>>. Default: the search.
|
|
| `--startsilent` | Start muted and stay muted until the first input of any kind, then play at the configured volumes. For an attract cabinet in a quiet room. `--nosound` outranks it. Default: off.
|
|
| `--trigger_threshold=PERCENT` | How far an analogue trigger must travel before it counts as a button, as a per cent of full travel -- Hypseus's unit, whose own default is `99.5`. `0` leaves the triggers on `DEAD_ZONE`, which is what Singe has always given them. Wins over `TRIGGER_THRESHOLD` in `controls.cfg`; `DEAD_ZONES` wins over both. `0` to `100`; see <<deadzones,Dead Zones and Triggers>>. Default: `0`.
|
|
| `--xratio=FACTOR` | The horizontal scale a light gun game reads with `ratioGetX()` to stretch its own gun coordinates for a display whose shape does not match the video. Singe does nothing with it; the game does the arithmetic, exactly as in Hypseus. `0` to `100`, kept to two decimals as Hypseus keeps it. Default: `0`, which every game that reads it treats as `1`.
|
|
| `--yratio=FACTOR` | The same for `ratioGetY()`. Default: `0`.
|
|
|===
|
|
|
|
Options may be given as `-x 640`, `-x640`, or `--xresolution=640`. Numeric
|
|
values are validated; a bad value prints the usage text and exits. The options
|
|
at the end of the table have no short letter; the alphabet ran out, and a letter
|
|
picked at random would help nobody.
|
|
|
|
Anything here except the options that name the game, name a directory, or must
|
|
act before a file can be found may also be set in a settings file, so a cabinet
|
|
does not need a launcher script for every game. See <<settings,The Settings
|
|
File>>.
|
|
|
|
[[apiversion]]
|
|
==== The Version Line
|
|
|
|
`--apiversion` prints one line and exits with status `0`:
|
|
|
|
----
|
|
singe version=3.00 protocol=1
|
|
----
|
|
|
|
This is a contract for front ends and will not change shape:
|
|
|
|
* The line ends with a newline and is the only thing written to standard
|
|
output. Nothing else is printed, whatever else is on the command line.
|
|
* The first field is always `singe`.
|
|
* Every field after it is `key=value`, separated by one space, with no spaces
|
|
inside a field.
|
|
* `version` is the release, the same number the banner shows.
|
|
* `protocol` is the version of this line's format. It becomes `2` only if the
|
|
rules above ever have to change.
|
|
* Later releases may add fields. A reader must ignore keys it does not know
|
|
rather than treating them as an error.
|
|
|
|
[[videorect]]
|
|
=== The Video Rectangle
|
|
|
|
Everything a game shows -- the disc, the overlay, the GUIs, the 3D scene and
|
|
the particles -- is drawn into one rectangle, and a handful of options decide
|
|
where that rectangle is. They apply in this order:
|
|
|
|
. *The window, or the bezel's cutout.* The rectangle starts as the whole
|
|
window. Singe works in the video's own coordinates and lets the window
|
|
letterbox them (`--stretch` fills instead), so nothing here depends on the
|
|
window's size. When `--bezel` loaded artwork that declares a cutout, the
|
|
rectangle starts as that hole instead and everything below places the picture
|
|
inside it; see <<bezels,Bezels>>.
|
|
. *`--scalefactor`.* The rectangle shrinks to that percentage of what it
|
|
started as, about its own centre.
|
|
. *`--shiftx` and `--shifty`.* The rectangle then moves inside the room the
|
|
scale left, `-100` hard against one side and `100` hard against the other,
|
|
which is what Hypseus's own `-shiftx` and `-shifty` mean. Two consequences
|
|
follow from that and are worth knowing: the picture can never be pushed off
|
|
the screen, and *at `--scalefactor=100` there is no room and a shift does
|
|
nothing at all*. To move the picture on a badly mounted panel, reduce the
|
|
scale first: `--scalefactor=90 --shiftx=-100` puts the shrunken picture hard
|
|
against the left.
|
|
. *`--sindengun`.* The border is applied last, to whatever the shift and the
|
|
scale left. The black ring takes the rectangle's edge, the white ring sits
|
|
inside it, and the game goes inside both, so a border is still a border
|
|
however the picture was placed. A scale small enough to leave no room for the
|
|
game squeezes the two rings rather than swallowing it. With bezel artwork
|
|
loaded the border goes to the window's edge instead and leaves the picture the
|
|
size it already was; `--sindenedge` decides it either way.
|
|
|
|
`--rotate` is not part of that order. The frame is drawn unrotated and the
|
|
finished picture is turned clockwise as a whole, so a shift moves the picture
|
|
along the video's own axes and turns with it. A quarter turn also swaps the
|
|
shape of the presented area, so a portrait window is filled rather than
|
|
letterboxed into a strip.
|
|
|
|
Mouse and light gun positions travel the same path backwards: the rotation
|
|
comes off first, and the position then maps into the rectangle, so a click
|
|
lands where the player sees it under any combination of these options.
|
|
`vldpGetScale`, `vldpSetScale`, `vldpGetRotate` and `vldpSetRotate` let a
|
|
script move the scale and the rotation while the game runs; the shift is a
|
|
display setting and has no script call. None of them is a `games.dat` key:
|
|
like `--scalefactor`, they describe the player's screen, not the game.
|
|
|
|
[[bezels]]
|
|
=== Bezels
|
|
|
|
A bezel is the artwork around an arcade cabinet's screen. Singe draws one from a
|
|
single image file, with the game's picture in a hole cut out of it, and can show
|
|
an arcade scoreboard in the space that leaves. Three options control it, and
|
|
they follow Hypseus's own convention so a cabinet's command line moves across
|
|
unchanged.
|
|
|
|
`--bezel cabinet.png` names the artwork. It is looked for in this order:
|
|
|
|
. `bezels/cabinet.png` in the game directory -- inside a packed `.game`, its own
|
|
copy, so a game can carry its bezel with it;
|
|
. `bezels/cabinet.png` in the data directory, where a user's own artwork can
|
|
live without touching a read-only game directory.
|
|
|
|
`--bezeldir art/cabinets` replaces the `bezels` folder with the one it names,
|
|
relative to the game directory or absolute, and then only that folder is
|
|
searched. Any format SDL_image reads will do (`.png` is the sensible choice,
|
|
because transparency matters). Artwork the option names and Singe cannot find
|
|
ends the program rather than running without it.
|
|
|
|
The artwork is drawn over the whole window, whatever shape either of them is, so
|
|
it stretches with the window exactly as the picture does. Give it the aspect
|
|
ratio you expect to play at.
|
|
|
|
==== The cutout
|
|
|
|
Beside the image, and named after it, a sidecar file says where the picture
|
|
goes: `bezels/cabinet.png` is described by `bezels/cabinet.cfg`. It is a Lua
|
|
file, as `controls.cfg` is, and sets one table in the artwork's *own pixels*:
|
|
|
|
[source,lua]
|
|
----
|
|
-- The hole in cabinet.png the picture is drawn in, in the artwork's own pixels.
|
|
CUTOUT = { x = 200, y = 120, width = 800, height = 500 }
|
|
----
|
|
|
|
All four fields are required and the rectangle must lie inside the artwork; a
|
|
sidecar that will not run, or that names a rectangle outside the image, ends the
|
|
program with a message. Because the numbers are the artwork's own, they stay
|
|
right at any window size: the image and the hole are scaled together.
|
|
|
|
With a cutout, the picture is drawn in that hole -- stretched to it, so the hole
|
|
decides the shape of the picture -- and `--scalefactor`, `--shiftx` and
|
|
`--shifty` then work *inside* it, shrinking and nudging the picture in the hole.
|
|
With no sidecar the picture keeps the whole window, exactly as it does with no
|
|
bezel, and those three options place it as they always have. See
|
|
<<videorect,The Video Rectangle>>.
|
|
|
|
The hole itself should be transparent in the image. Singe draws the artwork
|
|
behind the picture by default, so an opaque middle would simply be covered, but
|
|
`--bezelflip` puts the artwork in front and then only its transparency lets the
|
|
game through.
|
|
|
|
==== Draw order
|
|
|
|
Without `--bezelflip` the artwork is the first thing in the frame: the picture,
|
|
the overlay, the GUIs, the 3D scene and the particles all draw over it. With
|
|
`--bezelflip` the artwork draws in front of the picture instead, which is the
|
|
order Hypseus uses for its own bezels, so a frame with soft or shaped edges
|
|
overlaps the picture properly.
|
|
|
|
A script decides what happens to the overlay in that case:
|
|
<<setoverlayontop,`setOverlayOnTop(true)`>> draws the overlay and the GUIs above
|
|
the artwork, and `false` (the default, and Hypseus's) leaves the artwork on top
|
|
of them. With the artwork behind the picture the setting changes nothing,
|
|
because everything is already above it.
|
|
<<mainbezelloaded,`mainBezelLoaded()`>> tells a script whether artwork is loaded
|
|
and gives it the artwork's identifier, the same number Hypseus reports, so a
|
|
game written for a particular bezel can recognise it.
|
|
|
|
Screen space GUIs and the score panel always draw above the artwork, since the
|
|
point of them is to sit on it.
|
|
|
|
==== The Sinden border
|
|
|
|
A Sinden light gun watches the screen for a bright border. With bezel artwork
|
|
loaded that border belongs at the *edge of the window*, not around the picture,
|
|
because the gun's camera sees the whole screen, so that is where `--sindengun`
|
|
puts it as soon as a bezel is loaded. There it is drawn as two rings, after
|
|
everything else, so the artwork between the border and the picture stays visible
|
|
and nothing paints over the white ring. With no bezel the border goes around the
|
|
picture, as it always has, and shrinks it.
|
|
|
|
`--sindenedge video` or `--sindenedge window` overrides that choice in either
|
|
direction: a gun cabinet whose artwork is only a thin frame may still want the
|
|
border around the picture, and a cabinet with no artwork at all may want it at
|
|
the screen's edge.
|
|
|
|
==== The score panel
|
|
|
|
Sixteen of the Hypseus Singe games can drive an arcade scoreboard: credits, and
|
|
a score and a life count for each of two players. In Singe that scoreboard is
|
|
the RmlUi document `Singe/scoreBezel.rml`, styled by `Singe/scoreBezel.rcss`,
|
|
both written beside the other support files and both replaceable -- a theme or a
|
|
game may restyle the panel by editing them, exactly as it may restyle
|
|
`Singe/gui.rcss`. The shipped style is a seven segment look: every readout shows
|
|
its digits dimly lit behind the live value, the way a real display does.
|
|
|
|
The panel is *off* until a game asks for it with
|
|
<<scorebezelenable,`scoreBezelEnable(true)`>>, which is how Hypseus ships as
|
|
well, so a game that never asks behaves exactly as before and draws its own
|
|
scoreboard with sprites. <<scorebezelgetstate,`scoreBezelGetState()`>> is the
|
|
question every such game asks first.
|
|
|
|
Where it goes is decided for you: the panel takes the deeper of the bands the
|
|
picture leaves above and below it -- the room a bezel's cutout usually leaves --
|
|
centred on the picture, and lies along the bottom of the picture when neither
|
|
band is deep enough to hold it. It is drawn as a screen space GUI, so a game may
|
|
put its own GUIs on the artwork beside it with
|
|
<<guidrawscreen,`guiDrawScreen`>>.
|
|
|
|
Hypseus's `scoreBezelEnable` takes a second argument choosing among its five
|
|
scoreboard backends. Singe has one, so the argument is accepted and ignored.
|
|
|
|
== Frequently Asked Questions
|
|
|
|
*Why is it named Singe?*
|
|
|
|
Singe is the name of the dragon in Dragon's Lair. As Singe (the program) began
|
|
as an add-on to Daphne (the princess in Dragon's Lair) the Dragon's Lair theme
|
|
was kept.
|
|
|
|
*What is the difference between Daphne, Singe, Hypseus, and Singe 2.xx?*
|
|
|
|
Daphne is an actual laserdisc game emulator that can run a handful of classic
|
|
laser games. Daphne/Singe (Singe 1.xx) is the original version of Singe that
|
|
was an add-on for the Daphne emulator -- originally shipped as a game DLL for
|
|
Daphne, then later combined into a single binary with the Daphne emulation
|
|
features removed. Hypseus-Singe is an enhanced fork of Daphne with Singe 1.xx
|
|
and some Singe 2.xx support. Finally, Singe 2.xx is an all-new, built-from-
|
|
scratch upgrade of the original Singe. In short, if you want accuracy, use
|
|
Daphne or Hypseus if they support emulating the desired game. If it is a Singe
|
|
game, use Singe 2.xx, or try Hypseus.
|
|
|
|
*Why should we not call it an "emulator"?*
|
|
|
|
Emulators use the real software or ROMs from the original game and pretend to
|
|
be the machine they were originally intended to run on. From the game's
|
|
perspective, it is business as usual. Singe, on the other hand, requires that
|
|
every game be re-implemented (ported) to run on Singe. The end result may be
|
|
very similar, but how it works is entirely different.
|
|
|
|
*Can I run Singe 1.xx games on Singe 2.xx?*
|
|
|
|
Yes, probably. But you likely will not want to. Almost everything from Singe
|
|
1.xx has been converted and enhanced for Singe 2.xx.
|
|
|
|
*Why does the game I just installed not show in the menu?*
|
|
|
|
Whoever packaged the game failed to include a proper `games.dat` file.
|
|
|
|
*Why is HD video slow?*
|
|
|
|
Due to the way Singe accesses video files to provide frame seeking, it is
|
|
unable to offload video decoding to the video card. High definition video
|
|
requires a lot of CPU.
|
|
|
|
*Why does my audio stutter on Windows?*
|
|
|
|
Users have discovered that most Singe audio and stuttering problems are related
|
|
to their installed audio driver -- especially Realtek-based devices. Try
|
|
updating your sound drivers or switching to the generic Windows HD Audio
|
|
driver. Disable surround sound -- use stereo, not 5.1 or 7.1. If your drivers
|
|
do not allow you to use stereo, you can use Virtual Audio Cable to fix this:
|
|
|
|
. Install VB-CABLE (https://vb-audio.com/Cable/).
|
|
. Go to Windows sound settings (right-click sound on the taskbar, then
|
|
*Sounds*, then *Playback*).
|
|
. Right-click the *Cable Input* device and choose *Set as default device*.
|
|
(You will lose your current sound output temporarily.)
|
|
. Go to the *Recording* tab.
|
|
. Right-click *Cable Output*, then *Properties*, then *Listen*, and finally
|
|
*Enable Listen to This Device*. In the drop-down list, choose your actual
|
|
listening device.
|
|
. You should have sound again. Load up Singe and your audio will be working
|
|
and lag-free.
|
|
|
|
*Does 3D work on a Raspberry Pi?*
|
|
|
|
On a Raspberry Pi 4 or later, yes. They have both a Vulkan driver and OpenGL
|
|
ES 3.1, and Singe uses whichever it finds. The Pi 3 has only OpenGL ES 2.0,
|
|
which cannot express the scene's skinning or morph targets, so it runs 2D games
|
|
(2D physics included) exactly as before and refuses the first 3D call. The Pi 4
|
|
is the minimum for any game that uses the 3D Scenes chapter. The same applies to
|
|
the cheap handhelds: a Mali-G31 or G610 has the ES 3.1 the scene needs, whether
|
|
or not it has Vulkan.
|
|
|
|
*Why does my audio stutter on the Raspberry Pi?*
|
|
|
|
The Raspberry Pi OS now uses Pipewire as the default audio backend. Switching
|
|
to PulseAudio seems to fix the issue:
|
|
|
|
. Run `sudo raspi-config` from a terminal window.
|
|
. Select *Advanced Options*.
|
|
. Select *Audio Config*.
|
|
. Select *PulseAudio*.
|
|
. Press kbd:[Tab], select *Ok*, and let it reboot.
|
|
|
|
== Game Development
|
|
|
|
=== Lua
|
|
|
|
Singe uses the Lua programming language (http://www.lua.org) for scripting game
|
|
logic. Lua is fast, lightweight, object-oriented, easy to use, and actually
|
|
used in the games industry. A tutorial in Lua is beyond the scope of this
|
|
document. You can find video tutorials for Lua on the Kangaroo Punch YouTube
|
|
channel and additional documentation by searching the web.
|
|
|
|
=== Basic Rules
|
|
|
|
* Singe is cross-platform. Things you do not normally need to worry about on
|
|
Windows are important. Filenames are case-sensitive -- `MyScript.singe` and
|
|
`myscript.singe` are not the same file. You should also use UNIX path
|
|
separators -- `/` rather than `\`. This both makes your code cleaner (no
|
|
double-escaped backslashes) and works everywhere Singe is supported.
|
|
* Pick a distinctive name for your game folder. Do not use spaces.
|
|
* Use the included Singe Framework. Do not copy the framework into your game
|
|
scripts. By including the provided framework you help with future
|
|
compatibility as Singe is updated. Begin (or end) your game script with:
|
|
+
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
* Keep your game self-contained. If you build on a third-party framework or
|
|
share code between your games, copy it into your game directory. Never
|
|
reference a directory beside your game; the only file outside your game a
|
|
script may load is `Singe/Framework.singe`. A single-file game (see
|
|
<<singlefile,Single-File Games>>) packs exactly one directory, and a game
|
|
that reaches outside it cannot be packed.
|
|
* Stay out of the `Singe/` folder. This is managed by Singe and anything added
|
|
or changed here is subject to future deletion.
|
|
* Include a `games.dat`. This is extremely important for new users. While
|
|
other front ends and Singe-based menu systems exist, it is vital that you
|
|
include data for the built-in menu system. Almost all support issues are due
|
|
to missing or malformed `games.dat` files.
|
|
* Do not distribute non-essential files. Never ship Singe binaries or
|
|
Singe-provided scripts with your game. Do not ship index files generated
|
|
from the videos.
|
|
|
|
[#reloading]
|
|
=== Reloading While You Work
|
|
|
|
Run the game with `--reload` (`-R`) and Singe watches every loose script file
|
|
the game has loaded. Save a change to one of them and the game starts again
|
|
from its script a moment later: everything the script made goes (sprites,
|
|
sounds, fonts, videos, GUIs, the 3D scene, physics, emitters, the overlay
|
|
back to its default size, the effects volume and the mouse back to their startup
|
|
state), the engine stays (the window, the GPU, the controllers), the disc is
|
|
parked on its first frame again as at startup, and the script runs afresh,
|
|
so a 52 MB model is back on screen in the time it takes to load it rather
|
|
than the time it takes to start Singe. A key or button held through the reload is
|
|
ignored until it is released, as at a fresh start. `F5` reloads on demand,
|
|
and a script can ask for it with `singeReload()`. Packed games have nothing to watch; the option is for
|
|
a game in a directory. A script error on reload is printed and the game sits
|
|
empty until the next reload fixes it; the file stays watched, so saving the
|
|
fix is enough. Because the reload runs the same
|
|
teardown that quitting does, anything a script leaks shows up here first.
|
|
|
|
[[deterministic]]
|
|
=== Deterministic Test Mode
|
|
|
|
`--deterministic` is a testing facility, not a way to play. It makes the engine
|
|
ignore real time altogether: instead of asking the machine what time it is, the
|
|
engine runs on a virtual clock that moves a fixed step -- 15 milliseconds by
|
|
default -- exactly once a frame. Everything paced by time follows that clock:
|
|
physics steps, sprite and model animation, particles, navigation, the GUI's
|
|
animations and transitions, `os.clock()` and `singeGetTicks()`. The disc follows
|
|
it too: while it is playing it advances exactly one video frame for every frame
|
|
the engine draws, from wherever it was last parked, and a paused or searched
|
|
disc stays where it was put. Lua's generator and the engine's own are seeded
|
|
from a fixed number. Nothing waits for anything, so the game runs as fast as the machine
|
|
will let it.
|
|
|
|
The point of all this is that a screenshot taken at a fixed frame number is the
|
|
same picture in every run. Without it, frame 40 is a different moment each time
|
|
-- the physics has taken a different number of steps and the disc is on a
|
|
different video frame -- so two runs of the same unchanged game can differ by a
|
|
million pixels, and a real change can hide inside that noise.
|
|
|
|
What it costs is everything that depends on real time. Audio still plays on the
|
|
device's own clock, which no virtual clock reaches, so sound drifts away from the
|
|
picture and means nothing. The frame rate means nothing either, and neither does
|
|
anything a game measures in seconds. Never report a performance number, an audio
|
|
problem or a timing problem from a run in this mode, and never ship a launcher
|
|
that uses it.
|
|
|
|
The number the option carries, if it carries one, is both the size of the step
|
|
and the seed the generators start from, `1` to `1000`:
|
|
|
|
----
|
|
Singe --deterministic the default 15 ms step, seeded with 15
|
|
Singe --deterministic=20 a 20 ms step, seeded with 20
|
|
----
|
|
|
|
Two runs at the same number are identical; two runs at different numbers are each
|
|
repeatable but not the same as each other, which is how a test asks for a
|
|
different but equally reproducible run. It may be set in a settings file as
|
|
`deterministic` like any other option.
|
|
|
|
==== Reference screenshots
|
|
|
|
The scenes in `testScripts` each take a screenshot or two at a fixed frame number
|
|
and quit by themselves, so in this mode they make a set of reference pictures a
|
|
later change can be compared against pixel for pixel. To record them, run every
|
|
scene with the option and keep what lands in the data directory:
|
|
|
|
[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
|
|
----
|
|
|
|
The scenes that do not take the disc want their own options, the same ones the
|
|
table in `testScripts/README.md` gives them: `scene12` runs with no disc at all,
|
|
`scene17` with the disc, and `scene20` with `-C 720x480`, all three with
|
|
`SDL_GPU_DRIVER=nothing`; `scene27` to `scene35` want `-x 1280 -y 720
|
|
-C 1280x720` and no disc; `scene46` wants `-g '10 5'`; and `scene47` wants
|
|
`--bezeldir=testScripts/bezels --bezel=cabinet.png`. Copy the `singe*.png` files
|
|
out of `data/testScripts` after each run, since the next run numbers its
|
|
screenshots from zero again.
|
|
|
|
Comparing a later build is then a matter of running the same commands and asking
|
|
for the difference, which must be nothing:
|
|
|
|
[source,sh]
|
|
----
|
|
compare -metric AE reference/scene36-singe000.png new/scene36-singe000.png null:
|
|
----
|
|
|
|
=== Game Directory Layout
|
|
|
|
A Singe installation is a game directory containing the `Singe` support
|
|
directory the engine creates on first run, one directory per game, any
|
|
packed `.game` files, and a `data` directory for everything the games and
|
|
the engine write. The executable usually sits there too, but need not:
|
|
`--gamedir` names the game directory from anywhere, and the `Menu.sh` (or
|
|
`Menu.bat`) the engine writes there then runs the executable by its full
|
|
path. Otherwise the launcher searches beside itself for a file whose name
|
|
begins with `Singe` and runs the first one it finds, the same way the
|
|
launchers `BatMaker` writes do, so an upgrade that changes the executable's
|
|
version in its name does not leave a launcher pointing at a file that is no
|
|
longer there.
|
|
|
|
----
|
|
Singe/ Support files extracted by the engine
|
|
Framework.singe Loaded by every game (dofile it)
|
|
Menu.singe The bundled game menu
|
|
Tools.singe The menu's service tools (see The Service Tools)
|
|
MenuDocument.singe The menu drawn as a GUI document
|
|
MenuOverlay.singe The menu drawn into the overlay, for a machine with no GPU
|
|
Backdrop.singe The menu's intro and the grid behind it, drawn live
|
|
menuBackground.mkv The same backdrop recorded, for a machine with no GPU
|
|
menuIntro.flac Its sound, which fades out as the menu takes the screen
|
|
Menu.rml The document renderer's RmlUi document
|
|
menu.rcss Its style sheet, on top of gui.rcss
|
|
gui.rcss The shipped GUI theme
|
|
controls.cfg.example Template for input mappings
|
|
click.wav Used by the menu's audio delay and sound tests
|
|
Manual.pdf This manual
|
|
COPYING The GNU General Public License, version 3
|
|
LICENSES Third-party notices and asset credits
|
|
ActionMax/ One game
|
|
games.dat Menu entries for the games in this directory
|
|
38AmbushAlley.singe A script
|
|
frame_38AmbushAlley.txt Its framefile (or a video with the same base name)
|
|
sprite_*.png, sound_*.wav, font_*.ttf
|
|
DLe.game A game packed into one file (see Single-File Games)
|
|
data/
|
|
machine.cfg This machine's audio delay and picture geometry
|
|
ActionMax/ Indexes, trace.txt, screenshots, saves for that game
|
|
----
|
|
|
|
Paths inside a script are relative to the directory Singe was started from,
|
|
not to the script. Use the `DIR` global, which `Framework.singe` sets to the
|
|
script's own directory, so a game works no matter where it is installed:
|
|
|
|
[source,lua]
|
|
----
|
|
crosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
|
|
----
|
|
|
|
Everything Singe writes goes to the data directory (`singeGetDataPath()`),
|
|
`data/` beside the games unless `--datadir` says otherwise, so a game can
|
|
live on read-only media. Singe 2.x wrote saves and settings into the game's
|
|
own directory; a game that still does so fails on a read-only install, so
|
|
keep every `io.output` under `singeGetDataPath()`.
|
|
|
|
=== Packaging Your Game
|
|
|
|
A finished game ships as one `.game` file, described next. Nothing else is
|
|
needed: the player copies the file beside the Singe binary and the menu
|
|
lists it. Singe no longer installs archives.
|
|
|
|
[#singlefile]
|
|
=== Single-File Games
|
|
|
|
A game can be one file. `singe --pack` writes a game directory into an
|
|
SQLite database, and Singe runs it from there: scripts, sprites, sounds,
|
|
fonts, framefiles, and video all load straight out of the database, and
|
|
installing the game is copying the file next to the executable. The menu
|
|
lists every `.game` file it finds beside the game directories.
|
|
|
|
----
|
|
Singe --pack DLe DLe.game Pack the DLe directory into DLe.game
|
|
Singe DLe.game Run the first games.dat entry in it
|
|
Singe --entry=2 DLe.game Run its second entry
|
|
Singe --unpack DLe.game DLe Write the files back out
|
|
Singe --patch DLe.game fixes Replace files from a directory (or a patch database)
|
|
----
|
|
|
|
Nothing in a game has to change to be packed. Every name a script uses,
|
|
whether through `dofile`, `require`, `io.open` and its relatives,
|
|
`spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, a framefile, or a
|
|
`controls.cfg`, goes through one lookup. For a loose game that lookup is
|
|
the plain filesystem, relative to the directory Singe was started from,
|
|
exactly as before. For a packed game a name is tried in three places, in
|
|
this order, and the first hit wins:
|
|
|
|
. A loose directory named like the database without its extension
|
|
(`DLe/` beside `DLe.game`). This is how you keep editing a packed game,
|
|
and how a player mods one: files dropped there beat the packed copies.
|
|
. The game's data directory, under `files/`. Everything a game writes
|
|
with `io.output` or `io.open(name, "w")` lands here, so a saved
|
|
`game.cfg` beats the packed original and the database is never written.
|
|
. The database itself.
|
|
|
|
Inside a database, names are matched without regard to case, with either
|
|
path separator, and relative to the game root. A leading component equal to
|
|
the game's own directory name is ignored, so `DLe/Cfg/game.cfg`,
|
|
`Cfg/game.cfg`, and `DIR .. "Cfg/game.cfg"` all find the same file. The
|
|
name of the database itself (`DLe.game`, with or without a trailing
|
|
separator) is the game root, as `.` is from inside. Names
|
|
starting with `Singe/` or with the data directory stay on the filesystem;
|
|
`Singe/Framework.singe` and `singeGetDataPath()` work as always, and such a
|
|
name may use `..` as long as it stays below that prefix (the `controls.cfg`
|
|
at the root of the data directory is found that way). Any other relative
|
|
name with a `..` component resolves nowhere inside a packed game: `io.open`
|
|
returns `nil` with a message saying the name reaches outside the game,
|
|
`lfs.attributes` returns `nil`, and `spriteLoad` and its kind report the
|
|
file as missing. The game root is a directory, not a file: `io.open` on it
|
|
returns `nil`, and `lfs.mkdir` and `lfs.rmdir` refuse it. `lfs.dir` on a
|
|
packed directory lists the union of the three places, sorted without regard
|
|
to case; when a loose file and a packed one differ only in case, the loose
|
|
spelling is listed. `lfs.attributes` on a packed file answers as the real
|
|
`lfs` does: the whole table with one argument, one field by name (an
|
|
unknown name raises an error), or a table of yours filled in; a missing
|
|
file returns `nil`, a message and the error number.
|
|
`require("name")` finds `name.lua`, `name/init.lua`, or `name.singe` under the
|
|
script's directory, then under the game root.
|
|
|
|
`io.open`, `io.lines` and `io.input` on a name that lives only inside the
|
|
database hand back a file of Singe's own rather than one of Lua's. Lua's
|
|
file handle is a C `FILE *`, and a database row is not a file, so reading
|
|
one used to mean writing it out beside the game and opening that copy;
|
|
Singe's handle reads the row where it lies. It answers to everything Lua's
|
|
does -- `read` with every format (`"a"`, `"l"`, `"L"`, `"n"`, and a byte
|
|
count), `lines`, `seek`, `close`, `flush` and `setvbuf` -- `io.type` calls
|
|
it a file, `io.read` and `io.close` take it, and it closes itself when it
|
|
is collected or when a `<close>` variable goes out of scope. The one
|
|
difference is that writing to it is an error, because what it is reading is
|
|
inside the database and the database is never written. A game writing a
|
|
file gets a real one in its data directory exactly as before, and so does
|
|
every name that is not packed. `testScripts/packedIo` is the test that
|
|
holds all of this to Lua's own behaviour; its header says how to run it.
|
|
|
|
The packer refuses a directory that has no `games.dat`, that contains
|
|
`controls.dat`, `Framework.singe`, a file whose extension is `exe`, `sh`,
|
|
`bat`, or `cmd`, or an extensionless file whose name starts with `singe`,
|
|
that has a top level entry named like the directory itself (that would make the own-directory
|
|
rule ambiguous), or whose scripts and data files reach outside the game
|
|
with `..`; it reports every such file, with the line. Two files whose
|
|
names differ only by case cannot both be packed. Stale `.index` files are
|
|
skipped. `--unpack` refuses a database whose stored names would land outside the target
|
|
directory (an absolute path, a drive letter, or a `..` component). A game
|
|
that references another game's directory (a shared framework beside it)
|
|
must copy that directory inside first. Before the database is committed,
|
|
its `games.dat` is checked: every entry needs a `SCRIPT`, and each
|
|
`SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE`, and `ATTRACT` it names must be a
|
|
file in the database (or under `Singe/`); a bad path is reported with the
|
|
entry's title and the pack is refused. `--patch` runs the same check on the
|
|
patched game, so a patch cannot leave a dangling path behind either.
|
|
|
|
`--patch` takes either a directory laid out like the game, whose files
|
|
replace the packed ones, or a `.patch` file made by `--pack` from such a
|
|
directory (`Singe --pack fixes DLe-1.1.patch`; a patch needs no
|
|
`games.dat`). Files the patch does not name are left alone. Either way the
|
|
change is one transaction: the game is intact if it is interrupted. A patch
|
|
may also carry a `removed(path)` table naming files to delete. A `.patch`
|
|
can be unpacked like a game, but it cannot be run and the menu ignores it.
|
|
|
|
A packed game's data directory is named for the database and then for the
|
|
script's directory inside it: `DLe/` for a `DLe.game` whose script sits at
|
|
its root under the data directory (`data/DLe/` by default), which is where the loose
|
|
`DLe/DLe.singe` has it, so saves and settings are shared between a loose
|
|
install and a packed one; a script packed in a subdirectory
|
|
(`Game/Main.singe`) gets `DLe/Game/`. The video index goes there too.
|
|
|
|
NOTE: Earlier 3.00 builds gave a packed game whose script sat in a
|
|
subdirectory the data directory `Game/` under the base, which every archive
|
|
with that layout shared. Such a game's saves now belong under `DLe/Game/`;
|
|
move them by hand.
|
|
When Singe runs a game from a database the menu passes `CONTAINER` in the
|
|
`games.dat` entry it launches; the field is set by the menu, never by hand.
|
|
|
|
For your own data, `require("sqlite3")` gives scripts the
|
|
https://lua.sqlite.org[lsqlite3] binding to the same SQLite the engine
|
|
uses. Keep such databases in `singeGetDataPath()`:
|
|
|
|
[source,lua]
|
|
----
|
|
local sqlite3 = require("sqlite3")
|
|
local db = sqlite3.open(singeGetDataPath() .. "scores.db")
|
|
db:exec("CREATE TABLE IF NOT EXISTS scores (name TEXT, score INTEGER)")
|
|
for row in db:nrows("SELECT name, score FROM scores ORDER BY score DESC LIMIT 10") do
|
|
print(row.name, row.score)
|
|
end
|
|
db:close()
|
|
----
|
|
|
|
The file format, for anyone writing tools: three tables.
|
|
`meta(key, value)` holds `version` (1), `gamedir` (the packed directory's
|
|
name), `chunk` (chunk size in bytes, 4194304), and `packer`.
|
|
`assets(path, name, size, data)` has one row per file, `path` being the
|
|
lower case forward-slash key and `name` the author's spelling; files no
|
|
larger than the chunk size are in `data`, larger ones have `data` NULL and
|
|
live in `chunks(path, chunk, data)` as consecutive numbered pieces. Singe
|
|
opens the database read only and immutable, so it runs from read-only
|
|
media and never journals beside it.
|
|
|
|
=== Event Driven... Or Not?
|
|
|
|
Traditionally, Singe used an event-driven programming model -- Singe handles
|
|
everything and only calls your code when it needs to tell you something or
|
|
update the screen. This is efficient and is how most Lua-based game engines
|
|
work, but it can be verbose. As of Singe 2.10 there is a new _threaded_ model
|
|
that allows you to write procedural code.
|
|
|
|
==== Event Driven
|
|
|
|
With the event-driven programming model, Singe controls the main program loop
|
|
and is in charge of the order of execution. Singe automatically handles all
|
|
the details of decoding and presenting video and audio. It manages
|
|
controllers, mice, and keyboard input. When Singe needs something
|
|
game-specific, it calls part of your script. The most basic Singe script that
|
|
demonstrates all the existing callbacks looks like this:
|
|
|
|
[source,lua]
|
|
----
|
|
-- Singe Game Skeleton.
|
|
|
|
-- Load the Singe Framework.
|
|
dofile("Singe/Framework.singe")
|
|
|
|
-- Declare any global variables you need here.
|
|
|
|
function onControllerMoved(axis, value, which)
|
|
--[[
|
|
Reports which controller axis was moved as well as its current value.
|
|
(Range: -32768 to 32767.) This is used for analog devices. Digital
|
|
input is handled by onInput and onKey.
|
|
--]]
|
|
end
|
|
|
|
function onInputPressed(what)
|
|
--[[
|
|
When in keyboard MODE_NORMAL, input events are reported here when the
|
|
key or button is first depressed. For a full list of keys, buttons,
|
|
and controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
|
will be called with the keysym of the key pressed.
|
|
--]]
|
|
end
|
|
|
|
function onInputReleased(what)
|
|
--[[
|
|
When in keyboard MODE_NORMAL, input events are reported here when the
|
|
key or button is released. For a full list of keys, buttons, and
|
|
controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
|
will be called with the keysym of the key released.
|
|
--]]
|
|
end
|
|
|
|
function onKeyPressed(key, scancode)
|
|
--[[
|
|
When in keyboard MODE_FULL, input events are reported here when the
|
|
key is pressed. Both the keysym and scancode are returned. For a list
|
|
of available scancodes, see Singe/Framework.singe.
|
|
--]]
|
|
end
|
|
|
|
function onKeyReleased(key, scancode)
|
|
--[[
|
|
When in keyboard MODE_FULL, input events are reported here when the
|
|
key is released. Both the keysym and scancode are returned. For a
|
|
list of available scancodes, see Singe/Framework.singe.
|
|
--]]
|
|
end
|
|
|
|
function onMouseMoved(x, y, xr, yr, which)
|
|
--[[
|
|
Called when the mouse is moved. When in SINGLE_MOUSE mode, absolute
|
|
X and Y values as well as the relative change in position are
|
|
returned. For MANY_MOUSE mode, only the relative change is available
|
|
as well as which mouse was moved.
|
|
--]]
|
|
end
|
|
|
|
function onOverlayUpdate()
|
|
--[[
|
|
This is the only place you can safely perform drawing operations.
|
|
If you wish to display a targeting cursor, you will need to save the
|
|
mouse position from onMouseMoved in global variables and then use
|
|
those here to render the cursor.
|
|
--]]
|
|
|
|
-- Tell Singe if we changed the display or not.
|
|
return(OVERLAY_UPDATED) -- Or OVERLAY_NOT_UPDATED if no drawing was done.
|
|
end
|
|
|
|
function onShutdown()
|
|
-- Called when the user exits your game. Free loaded resources here.
|
|
end
|
|
|
|
function onSoundCompleted(id)
|
|
-- The sound "id" just finished playing.
|
|
end
|
|
|
|
-- Note: There is no "onStartup" event.
|
|
-- Any startup code you need can be placed here.
|
|
----
|
|
|
|
[#threaded]
|
|
==== Threaded
|
|
|
|
As of Singe 2.10, you can use the classic procedural programming model. To
|
|
use this model, declare a `singeMain()` function and include the Singe
|
|
framework at the *end* of your program rather than at the beginning. Lua is
|
|
not truly multithreaded, so this model relies on the game developer to
|
|
cooperatively multitask by calling `singeYield()` anywhere code consumes a
|
|
substantial amount of time. As with the event-driven model, Singe still
|
|
manages all input, video decoding, and audio decoding. A sample threaded
|
|
program looks like this:
|
|
|
|
[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
|
|
|
|
The two models combine freely. When `singeMain` exists, `Framework.singe`
|
|
installs its own `onOverlayUpdate`, but every other callback still fires:
|
|
define `onInputPressed`, `onSoundCompleted`, or `onShutdown` next to
|
|
`singeMain` and they are called between the coroutine's yields. A typical
|
|
split keeps the game's flow procedural while reacting to input and audio
|
|
events:
|
|
|
|
[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")
|
|
----
|
|
|
|
Do not define `onOverlayUpdate` yourself in this model; draw from inside
|
|
`singeMain` instead. An error raised inside `singeMain` ends the game with a
|
|
traceback, and returning from `singeMain` quits cleanly.
|
|
|
|
[#pausing]
|
|
=== Pausing
|
|
|
|
Every game gets a working pause for free. While the pause key is enabled
|
|
(the default), pressing the key mapped to `INPUT_PAUSE` makes the engine
|
|
pause the disc, every loaded video, and every sound, draw a PAUSED
|
|
indicator over the last frame, and stop running the script: no
|
|
`onOverlayUpdate`, no `singeMain` resumption, no input, mouse, controller,
|
|
or sound callbacks. Timers built on Lua libraries stop with it because
|
|
nothing runs them. Pressing the key again resumes everything that was
|
|
playing and the script continues where it left off. Only the engine's own
|
|
switches (pause, quit, screenshot, grab) still work while frozen.
|
|
|
|
Inputs are kept truthful across the freeze. Anything the script believes is
|
|
held down is released with `onInputReleased` (or `onKeyReleased` in
|
|
`MODE_FULL`) when the pause starts, and whatever is still physically held
|
|
when the pause ends is pressed again. A joystick held through a pause never
|
|
sticks.
|
|
|
|
A game that wants its own pause behavior (a pause menu, a story freeze that
|
|
the player may not skip) calls `singeSetPauseKeyEnabled(false)`. The key is
|
|
then delivered to the script as `SWITCH_PAUSE` and the game drives
|
|
`singeSetPauseFlag` itself, which pauses the media without freezing the
|
|
script.
|
|
|
|
In `MODE_FULL` the keyboard belongs entirely to the game, so keys mapped to
|
|
the engine's switches do nothing there (a `p` typed into a high score entry
|
|
must stay a `p`). Gamepad and mouse buttons mapped to those switches keep
|
|
working in either mode, since they cannot be typed.
|
|
|
|
[#gamesdat]
|
|
=== games.dat
|
|
|
|
The `games.dat` file allows Singe to automatically locate new games when they
|
|
are installed by the end user. This file is extremely important and must be
|
|
included with every Singe game. Place `games.dat` in the top-most directory
|
|
of your game. An example containing multiple games:
|
|
|
|
[source,lua]
|
|
----
|
|
GAMES = {
|
|
{
|
|
TITLE = ".38 Ambush Alley",
|
|
SCRIPT = "ActionMax/38AmbushAlley.singe",
|
|
VIDEO = "ActionMax/frame_38AmbushAlley.txt",
|
|
STRETCH = false,
|
|
NO_MOUSE = false,
|
|
RESOLUTION_X = 720,
|
|
RESOLUTION_Y = 480,
|
|
SINDEN_GUN = "",
|
|
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10",
|
|
CABINET = "ActionMax/cabinet_38AmbushAlley.png",
|
|
MARQUEE = "ActionMax/marquee_ActionMax.png",
|
|
ATTRACT = "ActionMax/video_38AmbushAlley.mkv",
|
|
ATTRACT_START = 3000,
|
|
ATTRACT_END = 3500,
|
|
YEAR = 1987,
|
|
PLATFORM = "ActionMax",
|
|
DEVELOPER = "Sourcing International, Ltd.",
|
|
PUBLISHER = "Worlds of Wonder, Inc.",
|
|
GENRE = "Shooter",
|
|
DESCRIPTION = "Get your target practice in with real police officers then hit the streets.",
|
|
CREATOR = "Scott Duensing",
|
|
SOURCE = "http://kangaroopunch.com"
|
|
},
|
|
-- ... additional entries ...
|
|
}
|
|
----
|
|
|
|
The keys `SCRIPT`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`,
|
|
`NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`,
|
|
`AUDIO_DELAY`, `AUDIO_SUFFIX`, `GAME_ID`, and `LEGACY_SPRITE_ARGS` are read by the engine when the menu (or your own
|
|
script, through `scriptExecute` / `scriptPush`) launches the entry; they
|
|
override the command line. The `VIDEO` line is the disc: an entry that names
|
|
one is a laserdisc game, and an entry that leaves it out or blank runs
|
|
without a disc and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see
|
|
<<withoutadisc,Games Without a Disc>>. `AUDIO_SUFFIX = "-es"` plays
|
|
`<base>-es.ogg` beside the disc's video instead of the audio inside it, for a
|
|
release whose other languages ship as separate files, the same thing
|
|
`--altaudio` does. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10
|
|
with the old sprite argument order (see <<migrating,Migrating from Singe
|
|
2.10>>). The remaining keys are read by the menu for display.
|
|
|
|
A script launched directly, by name or by its directory, gets the same
|
|
treatment: Singe looks for a `games.dat` in the script's directory and up to
|
|
three directories above it, and if an entry's `SCRIPT` names the launched
|
|
script (entry paths count from the directory above the `games.dat`, so
|
|
`ActionMax/games.dat` names `ActionMax/38AmbushAlley.singe`), that entry's
|
|
keys apply. The one difference from a menu launch is precedence: an option
|
|
typed on the command line (`--framefile`, `--disc`, `--stretch`, `--nomouse`,
|
|
`--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`,
|
|
`--altaudio`, `--canvas`) keeps its value, since the person typing it meant it.
|
|
A value from a settings file does not: it is a default, and the entry describes
|
|
the game better (see <<settings,The Settings File>>). A `games.dat` that fails
|
|
to run is reported and ignored for a direct launch.
|
|
|
|
[#withoutadisc]
|
|
=== Games Without a Disc
|
|
|
|
Singe no longer assumes a laserdisc. A game is a laserdisc game only when it
|
|
names one: a `VIDEO` in its `games.dat` entry, or `--disc` (or a
|
|
`--framefile`) on the command line. Everything else runs without a disc, and
|
|
the world is a *canvas* instead of a video frame: `CANVAS_X` / `CANVAS_Y` in
|
|
`games.dat` or `--canvas=WxH` on the command line, 720x480 by default so a
|
|
discless game looks like a standard definition laserdisc game unless it
|
|
asks otherwise.
|
|
|
|
Without a disc the engine draws on black, the overlay defaults to half the
|
|
canvas as usual, and the `disc*` and `vldp*` functions become harmless:
|
|
`discGetState` reports `DISC_EJECTED`, `discGetFrame` returns `0`,
|
|
`discGetWidth` / `discGetHeight` return the canvas, `vldpGetPixel` returns
|
|
black, and the transport calls trace and do nothing. Everything else -- the
|
|
overlay, sprites, fonts, sounds, extra videos through `videoLoad`, input,
|
|
and both programming models -- is identical. The global `SINGE_DISC` tells a
|
|
script which kind of game it is running as.
|
|
|
|
[#gui]
|
|
=== GUI
|
|
|
|
Menus, option pages, HUDs and high score entry used to be built out of
|
|
`fontPrint`, `overlayBox` and a hand rolled cursor. Singe 3 adds a GUI: a
|
|
document written in RML (an HTML-like markup) and styled by RCSS (a CSS-like
|
|
style sheet), laid out and drawn by RmlUi, with text wrapping, flexbox,
|
|
nine-slice panels from your own artwork, transitions, animations, form
|
|
controls and a focus model a gamepad can drive. A *GUI* is one RmlUi
|
|
context of a fixed size rendered into its own texture every frame, and that
|
|
texture goes wherever a texture can: composited over the overlay with
|
|
`guiDraw`, or shown on a material in the 3D scene with `materialSetGui`, so
|
|
the same options page works flat on the screen and on the monitor of a
|
|
modeled arcade cabinet. A GUI needs the GPU device the 3D scene uses, so it
|
|
is unavailable on a machine without one (see <<sceneperformance,Performance
|
|
and Requirements>>); `guiNew` raises an error rather than answering false, so
|
|
ask <<singehasgpu,singeHasGpu>> first if your game can do without. The bundled
|
|
menu does exactly that, and draws itself into the overlay instead when the
|
|
answer is no (see <<menurenderers,The two renderers>>). Every call named here has an entry in the
|
|
<<guiref,Gui>> section of the reference.
|
|
|
|
The language reference for RML and RCSS is RmlUi's own documentation at
|
|
https://mikke89.github.io/RmlUiDoc/, and it is worth reading before the
|
|
first document; this chapter covers what Singe adds on top.
|
|
|
|
==== Documents and Style Sheets
|
|
|
|
`guiNew(width, height)` makes a GUI and returns an integer handle; up to
|
|
sixteen exist at once. `guiLoad(gui, name)` loads an RML document into it,
|
|
shows it and returns a document handle (up to thirty-two per GUI);
|
|
`guiHide`, `guiShow` and `guiClose` do what their names say, and
|
|
`guiDelete` drops the GUI, its documents and its texture. A document is
|
|
usually the size of its GUI, and a GUI is usually the size of the overlay
|
|
for a full screen page or the size of the panel for a HUD element:
|
|
|
|
[source,lua]
|
|
----
|
|
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
|
local doc = guiLoad(gui, DIR .. "options.rml")
|
|
----
|
|
|
|
The document name goes through the virtual file system like a sprite's, so
|
|
it may live loose on disk or inside a packed game, and it is resolved
|
|
relative to the directory Singe was started in, so prepend `DIR` for files
|
|
shipped with your game, exactly as for `spriteLoad`. Names *inside* a
|
|
document (a style sheet's `href`, an image in a decorator, a sprite sheet's
|
|
`src`) resolve relative to the file that names them, as RmlUi does, and
|
|
then through the same lookup, so a document, its style sheet and its
|
|
images sit beside each other in the game directory and a packed game finds
|
|
them all. Two kinds of name are taken as they are: the engine's own files
|
|
under `Singe/`, and absolute paths.
|
|
|
|
The same rule read the other way lets a document that lives under `Singe/`
|
|
reach a game's files: a relative name resolves from the document's own
|
|
directory, so from `Singe/Menu.rml` the name `../ActionMax/cabinet.png` is
|
|
`ActionMax/cabinet.png` in the game directory, and a name under
|
|
`../DLe.game/` is a file inside that packed game. The bundled menu does
|
|
exactly this for the cabinet and marquee art of the selected game, setting
|
|
an image decorator on the art panel from Lua so the picture is fitted
|
|
inside it whatever its size:
|
|
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: fit a game's picture into one of the art panels.
|
|
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
|
|
----
|
|
|
|
An RML document is a `<head>` with a title, style sheet links or an inline
|
|
`<style>`, and a `<body>` of elements. A minimal one, using the shipped
|
|
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 draws nothing it has not been told the size of, and every element is
|
|
`display: inline` until a style says otherwise, so a style sheet is not
|
|
optional. `Singe/gui.rcss` is the theme the engine ships: it gives the
|
|
body, headings and paragraphs a size and a color, makes `div` a block, and
|
|
styles every form control. Link it first and override what you like in a
|
|
sheet of your own or an inline `<style>`; the later rule wins as in CSS.
|
|
Lengths in the theme are `dp`, RmlUi's density independent pixel, which is
|
|
a pixel at the GUI's own size.
|
|
|
|
Text needs a font face. The engine loads `Singe/FreeSansBold.ttf` for every
|
|
GUI as the *fallback* face (the face that serves any glyph no other face
|
|
has); its family name is `FreeSans` and its weight is bold, which is what
|
|
the theme's `body` rule asks for. `guiLoadFont(name)` adds one of the
|
|
game's own faces for every GUI, and `guiLoadFont(name, true)` makes it the
|
|
fallback instead. A document names a face by the family name inside the
|
|
file, not the file name, so load fonts before the documents that use them:
|
|
|
|
[source,lua]
|
|
----
|
|
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
|
|
----
|
|
|
|
[source,css]
|
|
----
|
|
h1 { font-family: Orbitron; font-weight: bold; }
|
|
----
|
|
|
|
RmlUi's RCSS effects are drawn in full. Children of a rounded or
|
|
transformed element are clipped to its shape (a scrolling list with
|
|
`border-radius` meets its frame at the curve), `filter: opacity()` fades
|
|
a group as one (the `opacity` property fades each piece on its own, as
|
|
RmlUi defines it), `mask-image` cuts an element out by an image's alpha,
|
|
`box-shadow` casts hard or soft shadows, and `filter` and
|
|
`backdrop-filter` take `blur`, `drop-shadow`, `brightness`, `contrast`,
|
|
`invert`, `grayscale`, `sepia`, `hue-rotate` and `saturate`. The
|
|
`linear-gradient`, `radial-gradient` and `conic-gradient` decorators
|
|
(repeating or not) draw as such; the shipped theme's panels, buttons and
|
|
progress bars use them. Three limits: the custom `shader` decorator is
|
|
not supported (it is refused with a warning), a gradient carries at most
|
|
sixteen colour stops (more are dropped), and effects nest at most eight
|
|
layers deep (a deeper element draws without its own layer). Every effect
|
|
renders through a layer the size of the GUI, and a blur through several
|
|
passes, so on a Pi keep them to a few elements and watch
|
|
<<guigetstats,guiGetStats>>, which counts the layers, passes and bytes a
|
|
frame cost.
|
|
|
|
==== Theming
|
|
|
|
A theme is a directory of RCSS and images, and a game ships its own look
|
|
the way it ships sprites. The usual shape is one sheet linked after
|
|
`Singe/gui.rcss` that restyles the theme's classes and adds its own, with
|
|
the artwork beside it. The shipped theme defines:
|
|
|
|
* `.panel` -- a bordered, rounded, translucent box with padding, the frame
|
|
of a page or a dialog.
|
|
* `.list` -- a scrolling box whose child `div` elements are rows: they take
|
|
focus, highlight on hover and focus, and a row with the `selected` class
|
|
stays highlighted. A vertical scrollbar appears when the rows overflow.
|
|
* `.muted` -- dimmer text, for hints and status lines.
|
|
* The form controls: `button` and `input.submit`, `input.text`,
|
|
`input.password` and `textarea`, `input.checkbox` and `input.radio`,
|
|
`input.range` with its track and slider, `select` with its arrow and
|
|
drop-down box, and `progress` with its fill; each with hover, focus and
|
|
active states, and each focusable with `tab-index: auto` so a gamepad can
|
|
reach it.
|
|
|
|
A panel from your own artwork is a nine-slice decorator over a sprite
|
|
sheet, and animations are RCSS keyframes, so a loading bar that fills by
|
|
itself needs no script at all:
|
|
|
|
[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
|
|
|
|
There are two APIs, and it helps to be clear which is which. The flat
|
|
`gui*` calls in Singe's own style create a GUI, load and draw documents,
|
|
route input and reach elements *by id*: `guiGetValue(gui, doc, id)` reads a
|
|
form control's value, or any other element's inner RML, as a string;
|
|
`guiSetValue(gui, doc, id, value)` writes one; and `guiSetHandler(gui, doc,
|
|
id, event, function)` calls a Lua function when the element fires an event
|
|
(`"click"`, `"change"`, `"submit"`, or any other RmlUi event name). The
|
|
function receives `(gui, document, id, event, value)`, with `value` the
|
|
element's value as a string at the moment of the event: a text field's
|
|
text, a `select` option's value, a range in RmlUi's own formatting
|
|
(`"48.000000"`), a button's inner RML. Everything a page needs day to day
|
|
is covered by those three calls:
|
|
|
|
[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)
|
|
----
|
|
|
|
Everything else inside a document is RmlUi's Lua API, which runs on the
|
|
game's own Lua state: a `<script>` block in a document, and `onclick` and
|
|
the other `on*` attributes, run with the game's globals, so they can call
|
|
your functions and the `gui*` calls alike. From the script side the same
|
|
objects are reachable through the `rmlui` global: contexts are named
|
|
`gui0`, `gui1`, ... after their handles, and documents are keyed by the
|
|
`id` of their `<body>`, so the button above is
|
|
`rmlui.contexts["gui0"].documents["options"]:GetElementById("go")`, and
|
|
`:DispatchEvent("click", {})` on it presses it from script (useful for
|
|
tests that run without a mouse). Elements, their attributes, classes,
|
|
styles and children, `data-model` bindings that mirror a Lua table into a
|
|
document, and the rest of the object model are documented with RmlUi's Lua
|
|
plugin at https://mikke89.github.io/RmlUiDoc/; `guiSetHandler` is the by-id
|
|
shortcut, not a replacement. A document closed through that API
|
|
(`document:Close()`) is forgotten by the engine, and its handle raises an
|
|
error afterwards.
|
|
|
|
==== Input
|
|
|
|
A new GUI only displays, which is right for a HUD or a sign;
|
|
`guiSetInput(gui, true)` makes it take input, and `guiSetInput(gui, false)`
|
|
turns that off again. For a GUI that takes input:
|
|
|
|
* *The mouse and a light gun* (which is a mouse) arrive in overlay
|
|
coordinates, mapped through the Sinden border and the overscan scale
|
|
exactly as for `onMouseMoved`, in `MOUSE_SINGLE` mode. The engine finds
|
|
the GUI under the pointer -- the last one drawn flat there this frame is
|
|
on top, and failing that a surface in the scene whose material shows one
|
|
(see below) -- and scales the position into that GUI's own pixels. A
|
|
press on an element is the GUI's; the left, right and middle buttons
|
|
click, and the wheel scrolls.
|
|
* *Keys* go to every GUI taking input, and a key that a focused element
|
|
used (a character in a text field, an arrow that moved focus, Return on a
|
|
button) is consumed. While a text or password field or a `textarea` has
|
|
focus the engine runs SDL's text input, so typed text arrives as text,
|
|
with the keyboard layout and any on screen keyboard or IME respected.
|
|
* *Pad and mouse switches* (the framework's `SWITCH_*` mappings from a
|
|
gamepad or mouse button) are translated into the keys a document
|
|
understands: the four directions become the arrow keys, `ACTION_1` is
|
|
Return and `ACTION_2` is Escape. A switch mapped to a keyboard key
|
|
already reached the GUI as that key.
|
|
|
|
The rule that makes a HUD safe is that *an event an element used never
|
|
reaches the game*: a consumed key does not arrive as `onInputPressed` or
|
|
`onKeyPressed`, a click on a button does not arrive as a mouse switch, a
|
|
translated direction that moved focus does not arrive as `SWITCH_UP`.
|
|
Everything a document did not use falls through to the usual callbacks, so
|
|
a HUD that takes input still lets the fire button through, and a page with
|
|
nothing focused lets every key through.
|
|
|
|
Focus and navigation are the document's own affair. The shipped theme
|
|
gives every control and every `.list` row `tab-index: auto`, so Tab moves
|
|
between them, Return activates the focused control, and arrow keys move
|
|
between controls with RmlUi's `nav` properties (`nav-up`, `nav-down`,
|
|
`nav-left`, `nav-right`, or `nav: auto` for all four). A page for a
|
|
cabinet with a stick and two buttons wants those on everything it expects
|
|
the player to reach:
|
|
|
|
[source,css]
|
|
----
|
|
button, .list div { nav: auto; }
|
|
----
|
|
|
|
[#guiscene]
|
|
==== A GUI in the Scene
|
|
|
|
`materialSetGui(material, gui)` shows a GUI's texture as a material's base
|
|
color texture, updated every frame, beside `materialSetVideo` and
|
|
`materialSetView`: a monitor, a cockpit panel, a sign. Pair it with
|
|
`materialSetUnlit` when the picture should not be shaded, and leave it lit
|
|
when it should. Nothing else changes; the material is lit, blended and
|
|
reflected like any other, and the same GUI can be drawn flat over the
|
|
overlay in the same frame.
|
|
|
|
`sceneProbeGui(x, y)` answers which GUI is on the surface under an overlay
|
|
point and where on it: a ray from the camera through the point against
|
|
every visible mesh whose material shows a GUI, returning the handle and the
|
|
pixel in that GUI's own coordinates, or `nil` when the point is off every
|
|
such surface. The engine asks it the same question when the mouse or a
|
|
light gun points at the scene and nothing drawn flat is under the pointer,
|
|
so a click on the monitor in a 3D cabinet presses the button under it
|
|
without any script involvement; the call is there for a game that wants to
|
|
know for itself, for a laser sight, a hover effect, or a 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
|
|
|
|
A GUI belongs to the script that made it. Quitting, `scriptPush`,
|
|
`scriptExecute` and a reload (see <<reloading,Reloading While You Work>>)
|
|
tear every GUI and document down with the rest of the script's state, and
|
|
the script makes them again when it runs; nothing from the `rmlui` object
|
|
model survives, so do not keep those objects in globals a reload would
|
|
reuse.
|
|
|
|
Every GUI updates and renders into its texture every frame whether or not
|
|
it is drawn anywhere, so a document keeps running its animations and
|
|
scripts off screen, and a GUI that is finished with should be deleted
|
|
rather than merely hidden. The cost is the size of the texture and the
|
|
complexity of the document: on a desktop a full screen page is cheap, on a
|
|
Raspberry Pi keep a GUI to the overlay's size or smaller and prefer one
|
|
GUI with several documents to several GUIs. Each font face costs an atlas
|
|
per size it is used at, so a page that uses one face at three sizes pays
|
|
for three atlases; a dozen sizes is a dozen. Sixteen GUIs may exist at
|
|
once, each with thirty-two documents, and sixteen `guiDraw` calls are
|
|
honored per frame.
|
|
|
|
==== A Worked Example
|
|
|
|
An options page over the disc: a panel from the shipped theme with a name
|
|
field, a volume slider, a difficulty list and two buttons, drawn over the
|
|
overlay and driven by the mouse or the stick. The document,
|
|
`options.rml`, beside the script:
|
|
|
|
[source,html]
|
|
----
|
|
<rml>
|
|
<head>
|
|
<title>Options</title>
|
|
<link type="text/rcss" href="Singe/gui.rcss"/>
|
|
<style>
|
|
body { width: 100%; height: 100%; }
|
|
.panel { margin: 30dp; }
|
|
.field { display: block; margin: 6dp 0; }
|
|
label { display: inline-block; width: 140dp; }
|
|
input.text { width: 300dp; }
|
|
button, input { nav: auto; }
|
|
#status { min-height: 30dp; }
|
|
</style>
|
|
</head>
|
|
<body id="options">
|
|
<div class="panel">
|
|
<h1>Options</h1>
|
|
<div class="field"><label>Name</label><input type="text" id="name" value="Dirk"/></div>
|
|
<div class="field"><label>Volume</label><input type="range" id="volume" min="0" max="63" step="1" value="48"/></div>
|
|
<div class="field"><label>Difficulty</label>
|
|
<select id="difficulty">
|
|
<option value="easy">Easy</option>
|
|
<option value="normal" selected>Normal</option>
|
|
<option value="hard">Hard</option>
|
|
</select>
|
|
</div>
|
|
<div class="field"><label>Attract</label><input type="checkbox" id="attract" checked/> plays the intro</div>
|
|
<div class="field"><button id="go">Start</button><button id="back">Back</button></div>
|
|
<p id="status" class="muted">Press Start when ready.</p>
|
|
</div>
|
|
</body>
|
|
</rml>
|
|
----
|
|
|
|
The script loads it into a GUI the size of the overlay, wires the three
|
|
elements it cares about, and draws it every frame until the player
|
|
presses Start; the disc plays underneath the whole time:
|
|
|
|
[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)
|
|
-- Only what the page did not use arrives here: nothing while it has
|
|
-- focus, everything once it is hidden.
|
|
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
|
|
----
|
|
|
|
The same page on the screen of a modeled cabinet is the two lines from
|
|
<<guiscene,A GUI in the Scene>> in place of `guiDraw`, and a click on the
|
|
monitor presses the button under it.
|
|
|
|
[#subtitles]
|
|
=== Subtitles
|
|
|
|
A game can ship a SubRip subtitle file and have Singe show it over the disc as it plays. Three calls do the whole job: <<srtload,`srtLoad`>> reads the file, <<srtenable,`srtEnable`>> switches it on, and <<srtposition,`srtPosition`>> says where the lines sit. <<srtclear,`srtClear`>> takes whatever is up off again. The feature came from Hypseus Singe and the four calls are its own.
|
|
|
|
[source,lua]
|
|
----
|
|
if srtLoad("subs/english.srt") then
|
|
srtPosition(80)
|
|
srtEnable(true)
|
|
end
|
|
----
|
|
|
|
==== The file format
|
|
|
|
SubRip (`.srt`) is a plain text file of numbered 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.
|
|
----
|
|
|
|
Each cue is a number on a line of its own, then a line holding two timestamps separated by `-->`, then one or more lines of text, then a blank line. A timestamp is `HH:MM:SS,mmm`: hours, minutes and seconds separated by colons, and milliseconds after a comma. The cue number is read and thrown away, so the numbers need not be in order or even correct.
|
|
|
|
The parser is deliberately forgiving in the places a hand-edited file goes wrong. Carriage returns, leading spaces and trailing spaces are stripped from every line, so a file written on Windows loads unchanged. A cue whose time line has no `-->`, or whose timestamps do not parse, is skipped and the file goes on loading. A cue with no text is skipped. A cue's lines are joined with line breaks and shown as written, and `<`, `>` and `&` reach the screen as themselves rather than being taken for markup.
|
|
|
|
What is *not* supported: the positioning coordinates some tools append to the time line are ignored (`srtPosition` decides the height), and SubRip's occasional `<i>` and `<b>` tags are shown as text rather than obeyed. Cues may overlap in time; the last one that has started wins.
|
|
|
|
`srtLoad` answers `false` and loads nothing when the file cannot be read, when nothing in it parses, or when there is no disc -- so a broken file is a `false` a game can act on and not a crash. It prints a warning naming the file in each case.
|
|
|
|
==== The timing
|
|
|
|
Timestamps are turned into *disc frame numbers* as the file is loaded, using the frame rate of the disc that is loaded at the time, and it is the frame number the engine matches every drawn frame. Two things follow from that.
|
|
|
|
First, **a subtitle survives a seek.** Sending the disc anywhere -- `discSearch`, `discSkipForward`, a step, a frame file crossing into another segment -- puts up the cue covering wherever it landed, on the next drawn frame, forwards or backwards. Nothing has to be reloaded and no clock has to be reset.
|
|
|
|
Second, **the file must be timed against the disc, not against the wall clock.** A `.srt` cut for a video at a different frame rate lands in the wrong place, and one loaded before the disc has been opened does not load at all. Load subtitles after the disc, which for an ordinary game means from the script's body or from `onStart`.
|
|
|
|
A cue is shown while the disc's frame is between its two frames inclusive. Only the cue is redrawn when it changes, so subtitles cost nothing while a line is up.
|
|
|
|
==== Styling
|
|
|
|
The lines are an RmlUi document, not a blitted text line, so they are styled the way a GUI is. Two files ship in the `Singe` folder and are extracted beside the others at first run:
|
|
|
|
* `Singe/subtitle.rml` -- the document. It has one element, `#slot`, that the engine writes the current cue or banner into.
|
|
* `Singe/subtitle.rcss` -- the look. `.cue` is the block that carries the height `srtPosition` asked for, as an inline `top`; `.cue .text` is the bar of text inside it, which by default is white on a dark rounded panel that hugs the text rather than spanning the picture.
|
|
|
|
Replace either file to restyle. The engine only ever sets `#slot`'s contents and the `.cue`'s `top`, so anything else -- the face, the size, the colours, the panel, a border, a shadow -- belongs in the RCSS and survives an engine upgrade. The document is drawn over the picture, above the overlay, below the screen space GUIs, follows the video rectangle wherever `--scalefactor`, `--shiftx`, a bezel cutout or `--rotate` put it, and takes no mouse input, so a click passes straight through it.
|
|
|
|
<<overlaybanner,`overlayBanner`>> uses the same document, so a banner and a subtitle look alike and a game that restyles one restyles both.
|
|
|
|
On a machine with no GPU there are no GUIs at all; `srtLoad` still parses the file and answers truthfully, and nothing is drawn. A warning says so once.
|
|
|
|
[#scenes3d]
|
|
=== 3D Scenes
|
|
|
|
Singe 3 draws a 3D scene between the laserdisc video and the 2D overlay.
|
|
Everything you already know still applies: sprites, text and the overlay sit
|
|
on top of the scene, and the video shows through wherever the scene's
|
|
background is transparent. A game that never calls a 3D function is
|
|
unaffected, and a game without a disc (see <<withoutadisc,Games Without a
|
|
Disc>>) can be a 3D game outright.
|
|
|
|
The sections below build up a scene in the order you will write one: nodes,
|
|
then meshes and materials, lights, the camera, the look of the frame as a
|
|
whole, models, and finally the things that sit on top of all that. Every
|
|
call named here has an entry in the <<reference,Lua API Reference>>.
|
|
|
|
==== What a Scene Is Made Of
|
|
|
|
Everything in a scene is a *node* in one tree, and node `0` is the root. A
|
|
node has a position, a rotation and a scale relative to its parent, and it
|
|
may carry one thing: a mesh with a material, a light, a sprite, or a line of
|
|
text. A loaded model becomes a subtree of nodes under one root, so the same
|
|
calls move a modeled character and a scripted cube alike.
|
|
|
|
The scene is right-handed with +Y up and -Z forward, like glTF and Blender's
|
|
export. Units are whatever your models use, and the primitives take the same
|
|
units; physics (see <<physics,Physics>>) assumes meters. Rotations are
|
|
degrees, given as three Euler angles applied as yaw, pitch and roll, or as a
|
|
quaternion when the value came from a model. A node's own forward is its -Z:
|
|
the camera looks down it, directional and spot lights shine down it, and
|
|
`nodeLookAt` turns it toward a world point.
|
|
|
|
There are two ways to fill a scene, freely mixed:
|
|
|
|
* *Build it from script* with the primitive meshes (`meshBox`, `meshSphere`,
|
|
`meshPlane`, `meshCylinder`, `meshCone`, `meshTorus`, `meshHeightmap`) or
|
|
your own vertex data (`meshNew`), plus materials, lights and a camera.
|
|
* *Load models* in glTF 2.0 binary form (`.glb`) with `modelLoad`, then place
|
|
them with `modelInstance` as many times as you like. Blender, Maya, 3ds
|
|
Max and every major engine export `.glb`.
|
|
|
|
A minimal scene is a mesh, a light and a camera:
|
|
|
|
[source,lua]
|
|
----
|
|
sceneEnable(true)
|
|
sceneSetBackground(0, 0, 0, 0) -- Transparent: the disc shows behind the scene.
|
|
|
|
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) -- One degree a frame about its own Y.
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
`sceneEnable(true)` is the one call every 3D game makes, and on a machine
|
|
that cannot draw 3D it is the call that fails (see
|
|
<<sceneperformance,Performance and Requirements>>). Without a camera the scene is viewed from `(0, 0, 5)`
|
|
looking at the origin, and without a background call the scene's background
|
|
is opaque black.
|
|
|
|
[#nodes]
|
|
==== Nodes
|
|
|
|
A node's transform is set outright (`nodeSetPosition`, `nodeSetRotation`,
|
|
`nodeSetQuaternion`, `nodeSetScale`) or nudged (`nodeMove`, `nodeRotate`),
|
|
always in its parent's space. `nodeSetParent` re-hangs a node, keeping its
|
|
local transform; `nodeGetWorldPosition` reads where it ended up after every
|
|
parent has had its say. `nodeSetVisible` hides a node and everything under
|
|
it, and `nodeDelete` frees it and its children, along with any body, emitter
|
|
or sound attached to them.
|
|
|
|
Names come from models: `nodeFind(name, root)` finds a joint or a part by
|
|
the name it had in the file, under the instance given as `root`. A scripted
|
|
node can be named too (`nodeSetName`), which is what `nodeFind` without a
|
|
root searches.
|
|
|
|
[source,lua]
|
|
----
|
|
-- A turret on a tank: the barrel is a child of the turret, the turret of the hull,
|
|
-- so turning the turret carries the barrel and moving the hull carries both.
|
|
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) -- Lay the cylinder along -Z.
|
|
|
|
function onOverlayUpdate()
|
|
local stick = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_X) / 32767 -- -1 to 1.
|
|
nodeRotate(turret, 0, stick * 2, 0)
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
Two calls bridge the scene and the overlay. `sceneProject(x, y, z)` turns a
|
|
world point into overlay coordinates, so a name tag or a health bar drawn
|
|
with `fontPrint` can follow a 3D object. `sceneUnproject(sx, sy, distance)`
|
|
goes the other way: the world point that far along the ray through an
|
|
overlay pixel, which with `physicsRaycast` is mouse picking.
|
|
|
|
[source,lua]
|
|
----
|
|
function onMouseMoved(x, y, dx, dy, which)
|
|
-- The mouse points along a ray from the camera; what it hits is the target.
|
|
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
|
|
|
|
A mesh is geometry; a material is how it looks; a node joins one of each
|
|
(`nodeSetMesh(node, mesh, material)`). Share both freely: one `meshBox` and
|
|
one material can sit on a thousand nodes, and that sharing is what makes the
|
|
renderer fast (see <<sceneperformance,Performance and Requirements>>).
|
|
`meshNew` builds a mesh from tables of positions, normals, texture
|
|
coordinates and indices when the primitives will not do.
|
|
|
|
A material starts as a base color with alpha (`materialSetColor`) and the
|
|
glTF metallic-roughness look: `materialSetMetallic` and
|
|
`materialSetRoughness` from `0` to `1`. Roughness near `0` gives a
|
|
mirror-sharp highlight and `1` a matte surface; a metal tints its highlights
|
|
and reflections with its own color and has no diffuse color of its own.
|
|
Beyond that:
|
|
|
|
* `materialSetEmissive` makes a surface give off light of its own color (a
|
|
screen, a lamp, hot metal); it does not light other things, but it does
|
|
bloom (see <<scenelook,The Look of the Frame>>).
|
|
* `materialSetUnlit` shows the color as it is, ignoring every light: HUD
|
|
elements in the scene, the built-in glow of a bulb's glass.
|
|
* `materialSetDoubleSided` draws the back of open shapes, such as a plane
|
|
seen from below or a flag.
|
|
* `materialSetBlend` makes the alpha count, for glass, ghosts and water.
|
|
Blended meshes draw after everything opaque, sorted by distance, and do
|
|
not cast shadows.
|
|
* `materialSetCutoff` keeps only the texels whose alpha reaches the
|
|
cutoff and throws the rest away, which is how leaves, fences, grates
|
|
and chain link are drawn: one quad, most of it gone. Unlike blending it
|
|
needs no sorting and it still casts a shadow, and the shadow is cut out
|
|
too, so a tree throws leaf shadows rather than the shadow of its quad.
|
|
What survives the cutoff is opaque: a half-transparent texel is either
|
|
kept whole or dropped, never blended, so nothing behind the scene (the
|
|
disc picture, most visibly) tints a leaf.
|
|
A cutoff of 0 (the default) turns masking off. A model loaded from glTF
|
|
brings its own: `MASK` sets the cutoff from the file, `BLEND` turns
|
|
blending on, and `OPAQUE` does neither.
|
|
|
|
The texture of a material can be any loaded sprite's image
|
|
(`materialSetTexture`), the laserdisc itself or a loaded video
|
|
(`materialSetVideo`), a second camera's picture (`materialSetView`, see
|
|
<<scenecamera,The Camera and Views>>), or a GUI document (`materialSetGui`,
|
|
see <<gui,GUI>>). A modeled arcade cabinet can play the disc on its own
|
|
screen, or show its options page there. Texture coordinates come from the mesh; the
|
|
primitives map an image once across each face, and `materialSetTiling`
|
|
repeats it, which is what ground and walls want. Textures are mipmapped and
|
|
filtered anisotropically, so a floor stays crisp into the distance;
|
|
`materialSetFilter(material, FILTER_NEAREST)` keeps pixel art blocky
|
|
instead.
|
|
|
|
[source,lua]
|
|
----
|
|
local stone = materialNew()
|
|
materialSetTexture(stone, spriteLoad(DIR .. "flagstones.png"))
|
|
materialSetTiling(stone, 12, 12) -- Twelve repeats across the plane.
|
|
materialSetRoughness(stone, 0.9)
|
|
|
|
local floor = nodeNew()
|
|
nodeSetMesh(floor, meshPlane(24, 24), stone)
|
|
|
|
local glass = materialNew()
|
|
materialSetColor(glass, 180, 220, 255, 90) -- Alpha 90 of 255.
|
|
materialSetBlend(glass, true)
|
|
materialSetRoughness(glass, 0.05)
|
|
----
|
|
|
|
Four more maps give a surface detail its geometry lacks, and a loaded model
|
|
brings them along when its file has them:
|
|
|
|
* `materialSetNormalMap` bends the lighting with a tangent-space normal map,
|
|
so bricks stand out of a flat wall. A strength above `1` exaggerates it.
|
|
* `materialSetOcclusionMap` darkens crevices that ambient light and the sky
|
|
cannot reach.
|
|
* `materialSetMetallicRoughnessMap` varies metallic and roughness across the
|
|
surface, packed the glTF way (roughness in green, metallic in blue) and
|
|
multiplied by the material's own factors, so set those to `1` when the map
|
|
should rule.
|
|
* `materialSetEmissiveMap` says which parts glow, tinted and scaled by
|
|
`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) -- The mortar glows orange.
|
|
----
|
|
|
|
.Compressed textures
|
|
A PNG is decoded to four bytes a texel on the GPU, plus a third again for
|
|
its mipmaps; a level the size of Sponza holds 390 MB of textures that way,
|
|
more than a Raspberry Pi 4 shares with its display. Textures shipped as KTX2
|
|
files (Basis Universal, made by `util/compressTextures.py` from PNGs or from
|
|
a whole `.gltf`) stay compressed on the GPU at a quarter of the size or
|
|
less, in whichever block format the machine has: BC7 on desktops, ASTC on
|
|
Apple GPUs and the Pi 4, BC3 on older cards, and plain RGBA where nothing
|
|
else exists, all from the one file. Give a `.ktx2` name in place of a sprite
|
|
to `materialSetTexture` and the map calls, and a model whose file uses
|
|
`KHR_texture_basisu` loads them on its own. `sceneGetStats` reports the
|
|
texture memory in use, so you can see the difference.
|
|
|
|
[source,lua]
|
|
----
|
|
materialSetTexture(ground, DIR .. "flagstones.ktx2")
|
|
local draws, drawn, calls, textureKB = sceneGetStats()
|
|
debugPrint("textures: " .. textureKB .. " KB")
|
|
----
|
|
|
|
==== Lights and Shadows
|
|
|
|
Up to eight lights shine at once, and each is a node, so lights parent and
|
|
animate like anything else:
|
|
|
|
* `LIGHT_DIRECTIONAL` is a sun: only its direction matters, and everything
|
|
is lit the same. An intensity of `1` lights a white surface facing it to
|
|
white.
|
|
* `LIGHT_POINT` is a bulb, fading with distance, and `lightSetRange` stops
|
|
it altogether beyond a distance. Its intensity is its brightness one unit
|
|
away, so a bulb that has to light a room needs a number in the tens.
|
|
* `LIGHT_SPOT` is a cone with an inner angle that is fully lit and an outer
|
|
angle where the light has faded to nothing (`lightSetCone`).
|
|
|
|
`sceneSetAmbient` adds a little light from everywhere so shadows are not
|
|
black; a sky does that better (see <<scenelook,The Look of the Frame>>).
|
|
Colors given as `0` to `255` are sRGB, like everything else in Singe, and
|
|
are converted to linear light on the way in, so a light of `128, 128, 128`
|
|
is a quarter as bright as white, not half.
|
|
|
|
Any light can cast soft-edged shadows from everything opaque
|
|
(`lightSetShadow`). Directional and spot lights cast through a shadow map;
|
|
point lights through a cube map that reaches every direction, so a bulb may
|
|
sit inside what it lights (a room, a lamp shade, the cabinet): closed meshes
|
|
cast from their back faces too, and the shadow's depth range is fitted to
|
|
the casters around the light. `nodeSetShadow(node, false)` excuses a mesh
|
|
from casting: the bulb's own glass, a glowing sign, a name tag.
|
|
`sceneSetShadowSize` sets the map's resolution (`256` to `4096`, default
|
|
`1024`) and trades sharpness for speed.
|
|
|
|
A directional light's shadow is split into cascades along the view
|
|
(`sceneSetShadowCascades`, three by default), each a map fitted to a slice
|
|
of what the camera sees, so shadows stay sharp close up in a large level and
|
|
still reach as far as `sceneSetShadowDistance`. One cascade fits a single
|
|
map to the whole scene, which suits small scenes and orthographic cameras.
|
|
A view (`viewNew`) draws with the same maps, taking for each point the
|
|
finest cascade that covers it, so a monitor shows the shadows the window
|
|
does.
|
|
|
|
[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) -- Three cascades out to 80 units.
|
|
|
|
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) -- A cube map: six passes a frame.
|
|
nodeSetShadow(bulbGlass, false) -- The glass around it casts nothing.
|
|
----
|
|
|
|
[#scenecamera]
|
|
==== The Camera and Views
|
|
|
|
Any node can be the camera (`cameraSet`); it looks down its own -Z, so
|
|
`nodeLookAt` aims it, and hanging it under a moving node makes a chase
|
|
camera. `cameraSetPerspective(fovDegrees, near, far)` is the default lens;
|
|
`cameraSetOrthographic(height, near, far)` draws without perspective, for
|
|
diagrams, HUD-like scenes and side-on games. Keep `near` as large as the
|
|
scene allows, because the depth buffer's precision is spent between `near`
|
|
and `far`.
|
|
|
|
[source,lua]
|
|
----
|
|
local camera = nodeNew()
|
|
nodeSetParent(camera, car)
|
|
nodeSetPosition(camera, 0, 2.5, 7) -- Behind and above the car, in its space.
|
|
nodeLookAt(camera, 0, 1, 0)
|
|
cameraSet(camera)
|
|
cameraSetPerspective(55, 0.1, 300)
|
|
----
|
|
|
|
A *view* is a second camera rendered to a texture every frame and shown on
|
|
a material: a security monitor, a rear-view mirror, a portal, a picture in
|
|
picture. `viewNew(width, height)` makes one at that resolution,
|
|
`viewSetCamera` gives it a camera node, and `materialSetView` puts its
|
|
picture on any mesh. Views share the frame's shadows and skip bloom, and
|
|
each one renders the scene again, so keep them few and small. Each view
|
|
turns billboards (see <<scenesprites,Sprites and Text in the Scene>>) to
|
|
its own camera and draws transparent materials back to front from it, so
|
|
a monitor sees name tags face on and glass in the right order.
|
|
|
|
[source,lua]
|
|
----
|
|
local rearCamera = nodeNew()
|
|
nodeSetParent(rearCamera, car)
|
|
nodeSetPosition(rearCamera, 0, 1.2, -2)
|
|
nodeSetRotation(rearCamera, 0, 180, 0) -- Looking back.
|
|
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) -- The plane faces up; stand it facing the driver.
|
|
----
|
|
|
|
[#scenelook]
|
|
==== The Look of the Frame
|
|
|
|
The scene is lit in linear light. Colors you give as `0` to `255` are sRGB
|
|
and are decoded on the way in, textures are decoded by the GPU as they are
|
|
sampled, lights add up without clipping, and at the end of the frame a tone
|
|
curve brings the result back to the screen. What that means in practice:
|
|
|
|
* `sceneSetExposure(stops)` brightens or darkens the whole frame like a
|
|
camera's exposure: `+1` doubles the light, `-1` halves it, so a cave and a
|
|
beach can share the same lights and materials.
|
|
* `sceneSetTonemap` picks the curve: `TONEMAP_NEUTRAL` (the default, which
|
|
keeps colors where glTF viewers keep them and compresses only the
|
|
brightest light), `TONEMAP_ACES` (a filmic look with more contrast), or
|
|
`TONEMAP_NONE` (clip at white).
|
|
* `sceneSetBloom(threshold, strength)` makes everything brighter than the
|
|
threshold glow: the sun through a window, a bulb, an emissive sign, a
|
|
flame. The glow is taken from the scene's light before the tone curve, so
|
|
only what is truly bright blooms; a threshold of `1` is white.
|
|
|
|
.The sky and its light
|
|
`sceneSetSky(file)` wraps the scene in an equirectangular panorama. A
|
|
Radiance `.hdr` keeps its full range; a PNG or JPEG works too. The sky is
|
|
drawn behind everything, and unless `sceneSetEnvironment(false)` it also
|
|
lights the scene: its overall color from every direction replaces the flat
|
|
ambient, and its reflection shows on metals and glossy surfaces, sharp on a
|
|
mirror and blurred as roughness rises. Without a sky, metals have nothing to
|
|
reflect and only the ambient and the lights shape them.
|
|
`sceneSetSkyIntensity` scales the sky and its light together, so one
|
|
panorama serves noon and dusk.
|
|
|
|
.Fog
|
|
`sceneSetFog(r, g, b, near, far)` fades everything, particles included,
|
|
toward a color between two distances. The sky is never fogged, so match the
|
|
fog's color to the sky's horizon and the far edge of the scene disappears
|
|
into it.
|
|
|
|
[source,lua]
|
|
----
|
|
sceneSetSky(DIR .. "harbor.hdr")
|
|
sceneSetSkyIntensity(0.8)
|
|
sceneSetFog(190, 200, 215, 40, 160) -- The color of the sky at the horizon.
|
|
sceneSetExposure(-0.5) -- A little darker than the sun would make it.
|
|
sceneSetBloom(1.0, 0.35) -- Only what is brighter than white glows.
|
|
----
|
|
|
|
==== Models and Animation
|
|
|
|
`modelLoad` takes a `.glb` by the same game-relative name `spriteLoad`
|
|
takes, and a packed `.game` carries it along. The file must be
|
|
self-contained: meshes, skins, animations and textures all inside the one
|
|
file, which is Blender's default export. A model that refers to a `.bin` or
|
|
an image beside it is refused with a message naming the file, for the same
|
|
reason a packed game has to be self-contained. Materials use the
|
|
metallic-roughness model with `KHR_materials_emissive_strength` and
|
|
`KHR_texture_basisu`; an alpha `MASK` material draws opaque, a texture
|
|
addressed through a second set of texture coordinates reads the first set,
|
|
and `KHR_texture_transform` is ignored, each with a line in the program
|
|
trace. A skin may have up to `128` joints. Loading is the expensive
|
|
part; `modelInstance` then places a copy of the model's node tree, as many
|
|
times as you like, and every instance shares the file's meshes and
|
|
materials. `modelDelete` frees the model; instances keep their nodes, bare.
|
|
|
|
[source,lua]
|
|
----
|
|
local foxModel = modelLoad(DIR .. "Fox.glb")
|
|
local pack = {}
|
|
for i = 1, 6 do
|
|
local fox = modelInstance(foxModel)
|
|
nodeSetScale(fox, 0.02) -- The file is in centimeters.
|
|
nodeSetPosition(fox, i * 1.5 - 5, 0, 0)
|
|
pack[i] = fox
|
|
end
|
|
----
|
|
|
|
A model's animations are listed by `modelGetAnimations` and driven per
|
|
instance with `animationPlay(node, nameOrIndex, loop, speed, fade)` on the
|
|
instance's root node, plus pause, resume, stop and seek calls. Node
|
|
animation (things moving), skinning (characters deforming) and morph targets
|
|
(blend shapes: faces, lip sync, muscle bulges) all work. Animations advance
|
|
with real time and hold while the game is paused. A morph weight can also be
|
|
set from script with `nodeSetMorph`, by name or number, on the node that
|
|
carries the mesh.
|
|
|
|
A `fade` in seconds crossfades from whatever was playing, so a walk becomes
|
|
a run without a snap. When nothing was playing (after `animationStop`, or
|
|
when a ragdoll has been switched off), the fade starts from the pose the
|
|
nodes are in, so a character eases up from where it fell. Up to four more
|
|
clips can play at once on layers above the base (`animationPlayLayer`), each
|
|
blended in by `animationSetLayerWeight` and limited to part of the skeleton
|
|
by `animationSetLayerMask`: a look-around on the neck while the legs walk, a
|
|
wave on one arm over a run.
|
|
|
|
[source,lua]
|
|
----
|
|
animationPlay(fox, "Walk", true)
|
|
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5) -- Head turns, blended in over half a second
|
|
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox)) -- ... on the neck and above only.
|
|
|
|
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) -- Faster, crossfaded in 0.4 s.
|
|
end
|
|
end
|
|
----
|
|
|
|
==== Sound in the Scene
|
|
|
|
A sound effect can be placed in the scene so it is heard from where it is.
|
|
`soundSetPosition(channel, x, y, z)` puts a playing channel at a world
|
|
point, `soundSetNode(channel, node)` makes it follow a node instead, and
|
|
`soundSetRange(channel, near, far)` says where it is at full volume and
|
|
where it has faded to silence. Sounds are heard from the camera unless
|
|
`soundSetListener` names another node (the player's head, for a third-person
|
|
game), and they pan left and right as the listener turns. A channel not
|
|
placed in the scene plays as it always has, with `soundSetPan` for stereo.
|
|
|
|
[source,lua]
|
|
----
|
|
local crackle = soundPlay(fireSound, -1) -- Loops forever.
|
|
soundSetNode(crackle, brazier)
|
|
soundSetRange(crackle, 2, 18) -- Full within 2 units, gone at 18.
|
|
soundSetListener(hero)
|
|
----
|
|
|
|
[#scenesprites]
|
|
==== Sprites and Text in the Scene
|
|
|
|
Any node can show a picture instead of a mesh. `nodeSetSprite(node, sprite,
|
|
height)` puts a loaded sprite (every frame of an animated one, with
|
|
`nodeSetSpriteFrame` choosing) on a quad that many world units tall, lit like
|
|
anything else and casting shadows unless `nodeSetShadow` says otherwise.
|
|
`nodeSetText(node, text, height)` does the same with a line of text in the
|
|
selected font and color, unlit. Both are meshes as far as the rest of the
|
|
engine is concerned: they parent, scale, and take a material's blend.
|
|
|
|
`nodeSetBillboard` turns a node to face the camera every frame:
|
|
`BILLBOARD_ALL` squarely (name tags, glows, impostors), `BILLBOARD_Y` about
|
|
its own up axis only (trees, health bars, anything that must stay upright),
|
|
and `BILLBOARD_NONE` to stop. It works on meshes too.
|
|
|
|
[source,lua]
|
|
----
|
|
local tag = nodeNew()
|
|
nodeSetParent(tag, fox)
|
|
nodeSetPosition(tag, 0, 110, 0) -- In the Fox's centimeters, above its head.
|
|
nodeSetScale(tag, 50) -- Undo the instance's 0.02 scale.
|
|
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)
|
|
----
|
|
|
|
[#sceneterrain]
|
|
==== Terrain
|
|
|
|
`meshHeightmap(image, sizeX, sizeY, sizeZ)` turns a grayscale image into
|
|
rolling ground: one vertex per pixel, black low and white `sizeY` high, with
|
|
normals from the slopes and texture coordinates across the whole. Give its
|
|
material a texture and `materialSetTiling` to repeat it every few units.
|
|
Images may be up to 1025 pixels each way; a 257 pixel map over 120 units
|
|
gives a vertex every half unit, which is plenty for hills.
|
|
|
|
A `BODY_STATIC` body with `SHAPE_MESH` on that node becomes a height field,
|
|
far cheaper than the triangles would be, so cars drive it and players walk
|
|
it as they are. `terrainGetHeight(node, x, z)` says how high the ground is
|
|
at a point, for placing things on it without a ray cast.
|
|
|
|
[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) -- A height field.
|
|
|
|
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
|
|
|
|
A navigation mesh tells characters where they can walk. `navNew(radius,
|
|
height, maxSlope, maxStep)` starts one for a given agent size, `navAddNode`
|
|
feeds it the level's geometry (a model instance's root does the whole
|
|
model), and `navBuild` bakes the walkable surface: floors, ramps and stairs
|
|
the agent can climb, minus a margin around every wall and column. Baking
|
|
Sponza takes a tenth of a second on a desktop; `navSave` and `navLoad` keep
|
|
a baked mesh in the game's data folder for machines where it takes longer,
|
|
and a mesh shipped with the game loads the same way. `navDraw` shows the
|
|
mesh over the scene while you are getting the parameters right.
|
|
|
|
Queries then answer the questions a game asks: `navPath` returns the corners
|
|
of the shortest route between two points, `navNearest` snaps a point onto
|
|
the mesh, `navRaycast` says whether a straight walk is blocked, and
|
|
`navRandomPoint` picks somewhere to wander to.
|
|
|
|
Agents (`navAgentNew`) walk the mesh for you. `navAgentMoveTo` sends one
|
|
somewhere and it steers around the other agents on the way, moving its node
|
|
(or driving a player controller on it, with `navAgentSetPlayer`) and calling
|
|
`onNavArrived` when it gets there. `navAgentGetVelocity` tells you whether
|
|
to play the walk or the run.
|
|
|
|
[source,lua]
|
|
----
|
|
local nav = navNew(0.4, 1.6, 45, 0.3) -- An agent 0.4 wide, 1.6 tall, climbing 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)) -- Off to the next spot.
|
|
end
|
|
----
|
|
|
|
[#sceneperformance]
|
|
==== Performance and Requirements
|
|
|
|
The scene needs a GPU that speaks Vulkan (Linux), Direct3D 12 (Windows 10
|
|
and later), Metal (macOS) or OpenGL ES 3.1. The ES backend is what brings in
|
|
the cheap Linux handhelds and the Raspberry Pi, whose Mali and VideoCore parts
|
|
have a mature GLES driver and either no Vulkan at all or an immature one; it is
|
|
chosen automatically when none of the other three is available, and it draws
|
|
the same picture from the same shaders. The Raspberry Pi 4 remains the minimum
|
|
Pi for 3D: the Pi 3 has only OpenGL ES 2.0, which cannot express the scene's
|
|
skinning or morph targets. On a machine without any of them, 2D games run
|
|
exactly as before (2D physics included) and the first 3D call ends the game
|
|
with an error naming the problem.
|
|
|
|
Meshes outside the camera's view are skipped, and copies of the same mesh
|
|
with the same material draw as one instanced call, so a forest of one tree
|
|
costs about what one tree costs. `sceneGetStats` reports the draws
|
|
collected, how many were in view, how many draw calls that took, and the
|
|
texture memory in use. To help it along:
|
|
|
|
* Share meshes and materials between nodes (one `meshBox`, many nodes; one
|
|
model, many `modelInstance` calls) rather than making one per object.
|
|
* Skinned and morphing meshes and blended ones draw one at a time; keep
|
|
them to what the scene needs.
|
|
* Antialiasing (4x multisampling) is on wherever the GPU offers it;
|
|
`sceneSetAntialias(false)` buys back speed on a Pi, as does a smaller
|
|
`sceneSetShadowSize` and fewer shadow-casting point lights, each of which
|
|
costs six passes.
|
|
* Keep an eye on triangle counts and texture sizes on small boards, ship
|
|
KTX2 textures, and load a model once rather than twice.
|
|
|
|
[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]
|
|
=== Physics
|
|
|
|
Bodies, joints, triggers and ray casts live in the 3D scene and ride on Jolt
|
|
Physics. A body attaches to a node the way a mesh does, and from then on the
|
|
node and the body are one thing: the body's shape is sized in world units
|
|
and scaled by the node, and whichever of the two moves, the other follows.
|
|
Units are meters, kilograms and seconds, and gravity defaults to
|
|
`0, -9.81, 0`. Physics does not need a GPU; see <<physics2d,2D Games>>.
|
|
|
|
==== Bodies
|
|
|
|
`bodyNew(node, type, shape, a, b, c)` gives a node a body of one of three
|
|
types:
|
|
|
|
* A *static* body (`BODY_STATIC`) stays where the node was when the body
|
|
was made: floors, walls, the cabinet. Moving its node afterwards does not
|
|
move the body.
|
|
* A *kinematic* body (`BODY_KINEMATIC`) follows its node wherever the script
|
|
or an animation moves it, and pushes dynamic bodies out of its way without
|
|
being pushed itself: a paddle, a moving platform, a character's hand.
|
|
* A *dynamic* body (`BODY_DYNAMIC`) is moved by the simulation and drives
|
|
its node: it falls, bounces, slides and gets pushed. Do not move its node
|
|
yourself; give it velocities, forces or impulses instead
|
|
(`bodySetVelocity`, `bodyApplyForce`, `bodyApplyImpulse`).
|
|
|
|
The shape is a box, sphere, capsule or cylinder sized by `a`, `b` and `c`
|
|
(`SHAPE_BOX` takes width, height and depth; `SHAPE_SPHERE` a radius;
|
|
`SHAPE_CAPSULE` and `SHAPE_CYLINDER` a radius and a height along Y), a
|
|
convex hull of the node's mesh (`SHAPE_HULL`, everything under the node, so
|
|
a model instance's root works), or the mesh's exact triangles
|
|
(`SHAPE_MESH`, static and kinematic bodies only; on a heightmap mesh it is
|
|
a height field, see <<sceneterrain,Terrain>>).
|
|
|
|
[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) -- The kinematic paddle sweeps through the stack.
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
.Mass and materials
|
|
A dynamic body's mass comes from its shape's volume at the density of
|
|
water, so a box the size of a door weighs 140 kilograms and a nudge will not
|
|
move it. Set `bodySetMass` on anything you want to feel light, and size
|
|
impulses to the mass: an impulse of `mass * speed` sets that speed.
|
|
`bodySetFriction` and `bodySetBounce` make ice and rubber; `bodySetEnabled`
|
|
takes a body out of the world and puts it back without losing it.
|
|
|
|
.Timing
|
|
The world steps at a fixed sixty times a second from the wall clock, after
|
|
animations have posed their nodes and before the scene renders, so a
|
|
kinematic body driven by an animation pushes with the right velocity.
|
|
Pausing the game pauses physics; `physicsSetEnabled(false)` pauses it on
|
|
its own. `bodyIsResting` says when a body has settled, which is when a
|
|
thrown thing has stopped and a score can be counted. A body, player,
|
|
ragdoll, soft body or vehicle whose node has been deleted is released at
|
|
the start of the next frame, enabled or not and paused or not, so its slot
|
|
is free again; a node handle that `nodeNew` later hands out afresh carries
|
|
none of the old node's physics, and `playerExists` and its kind answer
|
|
`false` for it until it gets its own.
|
|
|
|
==== Events and Queries
|
|
|
|
Define `onCollision(nodeA, nodeB, x, y, z, speed)` and Singe calls it once
|
|
for each new contact, with the point and the speed the two bodies met at,
|
|
so a bump and a crash can sound different. A body made a trigger with
|
|
`bodySetTrigger` pushes nothing but reports what enters and leaves it
|
|
through `onTrigger(trigger, other, entered)`: a finish line, a doorway, a
|
|
danger zone. `physicsRaycast` finds the first body along a ray; with
|
|
`sceneUnproject` for the ray, that is mouse picking (see <<nodes,Nodes>>).
|
|
|
|
[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
|
|
|
|
`jointHinge`, `jointBall` and `jointSlider` connect two bodies, or a body
|
|
and the world when the second node is `-1`: doors, pendulums, drawers, the
|
|
wheels of a side-on car. The anchor and the axis are given in world space
|
|
where the bodies stand when the joint is made. Hinges and sliders take
|
|
limits (`jointSetLimits`): a positive hinge angle is a right-hand turn of
|
|
the first node about the axis, and a positive slider distance is along it.
|
|
|
|
[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) -- On the frame, about Y.
|
|
jointSetLimits(hinge, 0, 110) -- Opens one way, 110 degrees.
|
|
----
|
|
|
|
Bodies on model instances work like any other: `bodyNew(instanceRoot,
|
|
BODY_DYNAMIC, SHAPE_HULL)` wraps the whole model in a convex hull. A skinned
|
|
character is best a player (below) or a kinematic capsule that pushes things
|
|
rather than being pushed.
|
|
|
|
[#physics2d]
|
|
==== 2D Games
|
|
|
|
Physics is not tied to the 3D scene. Nodes exist on every machine, GPU or
|
|
not, so a 2D game can give nodes bodies, call `physicsSet2D(true)` so they
|
|
move in X and Y and turn about Z only, work in overlay coordinates with
|
|
gravity pointing down the screen, and draw sprites where the nodes are.
|
|
This is the form that runs on a Raspberry Pi 3, which has no 3D.
|
|
|
|
[source,lua]
|
|
----
|
|
physicsSet2D(true)
|
|
physicsSetGravity(0, 900, 0) -- Pixels per second squared; overlay Y runs down.
|
|
|
|
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
|
|
----
|
|
|
|
Give every 2D body some depth (the third size) so shapes overlap in Z; `50`
|
|
is fine. Circles are `SHAPE_SPHERE`. Players and 2D emitters (see
|
|
<<particles,Particles>>) work in this world too, and a platformer is a
|
|
player with `playerMove(node, vx)`.
|
|
|
|
==== Players
|
|
|
|
A body goes where forces send it; a *player* goes where the script says.
|
|
`playerNew` puts a character controller on a node: a capsule by default
|
|
(`playerNew(node, radius, height)`), or any convex `bodyNew` shape
|
|
(`playerNew(node, SHAPE_BOX, w, h, d)` for a robot, `playerNew(node,
|
|
SHAPE_SPHERE, r, 0)` for something that rolls yet still climbs steps,
|
|
`SHAPE_HULL` for the model's own outline; three arguments always mean a
|
|
capsule, so a sphere spells its sizes out). The shape stands on the node's origin, which is the feet. Each
|
|
frame the script gives `playerMove` the horizontal velocity it wants, and
|
|
the controller walks along floors, climbs steps up to `playerSetStep`,
|
|
slides along walls, refuses slopes steeper than `playerSetSlope`, falls
|
|
under gravity, rides moving platforms, and shoves light dynamic bodies out
|
|
of the way (`playerSetPush`). `playerJump` works only with ground underfoot
|
|
and says whether it did. The node's rotation is the script's: turn it to
|
|
face the way it walks, and hang the model under it as a child.
|
|
|
|
[source,lua]
|
|
----
|
|
hero = nodeNew()
|
|
nodeSetPosition(hero, 0, 0, 0)
|
|
playerNew(hero, 0.3, 1.0) -- A capsule of radius 0.3, 1 tall, feet at the node.
|
|
fox = modelInstance(modelLoad(DIR .. "Fox.glb"))
|
|
nodeSetParent(fox, hero)
|
|
|
|
function onInputPressed(switch)
|
|
if switch == SWITCH_BUTTON1 then
|
|
playerJump(hero, 4.5) -- Needs ground underfoot.
|
|
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
|
|
----
|
|
|
|
Players report `onCollision` when they start touching a body and
|
|
`onTrigger` when they enter or leave a sensor, with their node in the place
|
|
a body's node would take, so a door, a pickup and a lava pit work the same
|
|
for a player as for a ball. `playerIsOnGround` and `playerGetGround` say
|
|
what the player stands on, for footstep sounds and for dropping off a
|
|
platform on purpose. In a 2D world a player is a platformer character:
|
|
`playerMove(node, vx)` runs along X, gravity points down the screen, and
|
|
everything else is the same. The controller's tolerances scale with the
|
|
shape, so a 30 pixel capsule in a sprite game behaves like a 0.3 meter one
|
|
in a 3D game.
|
|
|
|
==== Vehicles
|
|
|
|
A *vehicle* is a dynamic body (the chassis, made with `bodyNew` like any
|
|
body, facing -Z with Y up) that Jolt drives through wheels:
|
|
`vehicleNew(node, VEHICLE_CAR)` for anything with axles,
|
|
`VEHICLE_MOTORCYCLE` for two wheels in a line that lean through turns and
|
|
balance, or `VEHICLE_TANK` for wheels on two tracks that turn on the spot.
|
|
Wheels are nodes: `vehicleAddWheel(node, wheelNode, radius, width,
|
|
suspension)` takes the wheel's position from where its node is relative to
|
|
the chassis, and from then on the engine poses that node every frame (its X
|
|
axis is the axle, so a `meshCylinder` wheel wants a child turned 90
|
|
degrees). Wheels found in a model with `nodeFind("wheelFL", car)` work the
|
|
same. By default the front wheels steer and all of them drive;
|
|
`vehicleSetWheel` says otherwise.
|
|
|
|
Each frame the script hands over the driver: `vehicleDrive(node, forward,
|
|
right, brake, handBrake)`. The engine, automatic gearbox and differentials
|
|
are Jolt's with a 1500 kg car's defaults; `vehicleSetEngine`,
|
|
`vehicleSetGears`, `vehicleSetSuspension`, `vehicleSetSteering`,
|
|
`vehicleSetBrakes` and `vehicleSetAntiRoll` tune them, and any change
|
|
rebuilds the vehicle in place at the next step. `vehicleGetSpeed`,
|
|
`vehicleGetRpm` and `vehicleGetGear` feed a dashboard. A vehicle is still a
|
|
body: it jumps ramps, hits walls, shoves crates and reports `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 }, -- Front left and right ...
|
|
{ -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 }, -- ... rear left and right.
|
|
}
|
|
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
|
|
----
|
|
|
|
Tanks take `forward` as throttle and `right` as the turn (with no throttle
|
|
they pivot), and ignore the steering, brake and anti-roll tuning, since
|
|
their tracks brake and turn on their own. The motorcycle's lean spring is
|
|
scaled to the chassis, so a light bike balances as well as a heavy one.
|
|
Wheeled vehicles need a 3D world; a side-on 2D
|
|
car is a body with two hinged wheels, which the joint calls already build.
|
|
|
|
==== Water
|
|
|
|
`bodySetWater(node, density, linearDrag, angularDrag)` fills a static body
|
|
with water: the top of its box is the surface, and every dynamic body
|
|
inside floats, sinks or rolls by Jolt's buoyancy against that surface (the
|
|
submerged part of the shape counts, so a tilted crate rights itself),
|
|
slowed by the drags. `bodySetCurrent` gives the water a flow. `density` is
|
|
a multiplier, `1` for water, and each floating body's own `bodySetBuoyancy`
|
|
(default `1.2`, which floats gently; below `1` sinks) scales it. The volume
|
|
is a trigger, so `onTrigger` fires on the way in and out for splashes.
|
|
Water is not drawn: give the volume's node a blended material, or none.
|
|
|
|
A player in water swims: `playerIsSwimming` says so, `playerMove(node, vx,
|
|
vy, vz)` then steers in three axes, it sinks slowly with no vertical input
|
|
(`playerSetSwim` sets that rate and the water's drag), and the current
|
|
carries it. A boat is a buoyant hull that `vehicleNew(node, VEHICLE_BOAT)`
|
|
gives a propeller and a rudder: `vehicleSetThrust` says how hard it pushes
|
|
and where in the hull, `vehicleSetRudder` how hard it turns, and
|
|
`vehicleDrive` works as for a car, all only while the propeller is under a
|
|
surface.
|
|
|
|
[source,lua]
|
|
----
|
|
pool = nodeNew()
|
|
nodeSetMesh(pool, meshBox(8, 3, 12), blue) -- Blended blue, drawn as the water.
|
|
nodeSetPosition(pool, 0, -1.5, 0) -- Its top, at Y = 0, is the surface.
|
|
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
|
|
bodySetWater(pool, 1.0, 0.6, 0.2)
|
|
bodySetCurrent(pool, 0.5, 0, 0) -- Drifting toward +X.
|
|
bodySetBuoyancy(crate, 1.6) -- Rides high.
|
|
bodySetBuoyancy(anchor, 0.3) -- Goes down.
|
|
----
|
|
|
|
==== Ragdolls
|
|
|
|
`ragdollNew(node)` looks under a model instance for its skin and turns the
|
|
skeleton into a ragdoll recipe: every joint with a child joint becomes a
|
|
capsule along that bone, jointed to the nearest bone above it by a
|
|
swing-twist joint with sensible limits, and leaf joints (fingers, ears, the
|
|
tip of a tail) ride along with their parents. Nothing moves until
|
|
`ragdollActivate`: then the animation stops, bodies are made where the
|
|
bones are, and from that moment physics drives the joint nodes, so the
|
|
skinned mesh flops, hits the floor and comes to rest with the rest of the
|
|
world.
|
|
|
|
`ragdollApplyImpulse` shoves one bone by its joint's name.
|
|
`ragdollSetStrength` powers every joint toward the pose the ragdoll was
|
|
activated in, which turns a limp fall into a stagger or lets a downed
|
|
character pull its limbs back in; it does not stand a body up, since
|
|
nothing pushes off the floor. `ragdollDeactivate` hands the skeleton back
|
|
to animation from wherever it lies, and an `animationPlay` with a fade then
|
|
eases it up from there. Tune a bone that looks wrong with `ragdollSetJoint`
|
|
before activating.
|
|
|
|
[source,lua]
|
|
----
|
|
fox = modelInstance(foxModel)
|
|
ragdollNew(fox) -- Recipe from the skin; still animated.
|
|
function onCollision(a, b, x, y, z, speed)
|
|
if (a == hero or b == hero) and speed > 3 then
|
|
playerSetEnabled(hero, false)
|
|
ragdollActivate(fox) -- Physics drives the skeleton now.
|
|
ragdollApplyImpulse(fox, "b_Spine02_03", 40, 30, 0)
|
|
end
|
|
end
|
|
----
|
|
|
|
==== Soft Bodies
|
|
|
|
Cloth, rope and jelly are meshes whose vertices are simulated.
|
|
`softNew(node, SOFT_CLOTH)` turns the node's mesh into cloth, its vertices
|
|
(welded by position) into particles joined by stretch, shear and bend
|
|
constraints, where the node has put it in the world; a subdivided
|
|
`meshPlane(width, depth, columns, rows)` is the usual start. `softNew(node,
|
|
SOFT_BODY)` does the same for a closed mesh and `softSetPressure` inflates
|
|
it: a balloon, a jelly. `softNew(node, SOFT_ROPE, x, y, z, segments,
|
|
radius)` runs a chain of particles from the node's position to that point
|
|
and gives the node a tube mesh around them (set its look with
|
|
`nodeSetMaterial`).
|
|
|
|
Soft bodies collide with everything rigid, fall under gravity, and are
|
|
drawn by the mesh they came from, which the engine rewrites every frame.
|
|
`softPin` holds the nearest particle where it is or to a node (the flag's
|
|
edge to the pole, the rope's top to the crane); `softSetStiffness`,
|
|
`softSetMass` and `softSetDamping` tune the material. A node whose mesh is
|
|
shared with other nodes changes them too, so give a soft body a mesh of its
|
|
own.
|
|
|
|
[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) -- Hanging upright.
|
|
softNew(flag, SOFT_CLOTH)
|
|
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
|
|
softPin(flag, -5, y, 0) -- Along the pole.
|
|
end
|
|
----
|
|
|
|
Cost scales with the particle count: a 20 by 20 sheet is 441 particles and
|
|
fine on a Raspberry Pi 4; keep cloth to a few hundred particles and a rope
|
|
to a dozen segments. Particles collide with rigid bodies, not faces, so a
|
|
fine enough mesh (particles closer than the smallest thing that will land
|
|
on it) is what stops a crate falling through. There is no self-collision:
|
|
cloth can pass through itself when crumpled. Skinned meshes cannot be soft.
|
|
|
|
==== Seeing the Physics
|
|
|
|
When a crate falls through the floor or a joint bends the wrong way,
|
|
`physicsSetDebug` draws what the simulation actually holds as lines over
|
|
the scene each frame:
|
|
|
|
* `DEBUG_SHAPES`: the bodies in wireframe, colored by state (sleeping,
|
|
moving, static), with the players in yellow.
|
|
* `DEBUG_CONSTRAINTS`: the joints, ragdoll joints and vehicle constraints
|
|
with their limits.
|
|
* `DEBUG_CONTACTS`: a red cross where bodies met this step.
|
|
* `DEBUG_VELOCITIES`: a line along each moving body's velocity.
|
|
* `DEBUG_STATIC`: the static bodies' shapes as well, which stay off
|
|
otherwise because a level mesh is a lot of lines.
|
|
|
|
Add them together, or use `DEBUG_ALL` for the first four. `DEBUG_NONE`
|
|
turns it off, and it costs nothing while off. A script draws its own lines
|
|
the same way with `lineDraw` (a ray cast, a path, a bounding box), one frame
|
|
at a time like the overlay, and `navDraw` shows a navigation mesh.
|
|
|
|
[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
|
|
|
|
Physics runs on the CPU and needs no GPU, but the library is built for
|
|
SSE4.1 on x86 (2008 and later) and NEON on the Pi; a machine below that
|
|
gets an error from the first `bodyNew`. The simulation uses every core but
|
|
one, so a desktop steps in parallel and a Pi on a single thread.
|
|
|
|
[#particles]
|
|
=== Particles
|
|
|
|
Sparks, smoke, fire, rain, dust, magic: anything made of many small
|
|
short-lived things is a job for an *emitter*. An emitter holds a recipe
|
|
(how many a second, how long they live, how fast and which way they set
|
|
off, what pulls on them, how they grow, fade and spin) and a pool of
|
|
particles it runs from that recipe. The script sets the recipe once, then
|
|
starts the emitter for a steady stream (`emitterStart`), bursts it for a
|
|
one-off puff (`emitterBurst`), or both. The engine moves every live
|
|
particle every frame, and stops moving them while the game is paused.
|
|
|
|
One emitter API serves the overlay and the 3D scene; only the units and
|
|
the drawing differ. Everything given as a minimum and a maximum
|
|
(`emitterSetLife`, `emitterSetSpeed`, `emitterSetSize`) is drawn at random
|
|
between the two for every particle.
|
|
|
|
==== 2D Emitters
|
|
|
|
`emitterNew()` with no argument makes a 2D emitter. It lives in overlay
|
|
coordinates, Y down, so a fountain's gravity is positive Y. Move it with
|
|
`emitterSetPosition`, and draw it from `onOverlayUpdate` with
|
|
`emitterDraw`. The overlay itself is a picture the CPU paints, so particles
|
|
are not painted into it: the GPU draws them, either above the whole overlay
|
|
(`PARTICLE_OVER`, the default) or beneath it and above the video and the 3D
|
|
scene (`PARTICLE_UNDER`, for smoke behind the score). 2D emitters run on
|
|
every machine Singe runs on, the Raspberry Pi 3 included, and go well with
|
|
2D physics: read a body's node position and give it to `emitterSetPosition`.
|
|
|
|
[source,lua]
|
|
----
|
|
sparks = emitterNew()
|
|
emitterSetBlend(sparks, PARTICLE_ADD)
|
|
emitterSetLife(sparks, 0.3, 0.7)
|
|
emitterSetSpeed(sparks, 80, 200)
|
|
emitterSetDirection(sparks, 0, -1) -- Up the screen.
|
|
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
|
|
|
|
`emitterNew(node)` makes a 3D emitter attached to a node. Particles are born
|
|
at the node's world position (spread over `emitterSetRadius`), in world
|
|
units, and draw themselves in the scene as camera-facing billboards after
|
|
every mesh: depth-tested against the geometry, never writing depth, unlit
|
|
unless asked, casting and receiving no shadows. By default a particle stays
|
|
where it was born once it leaves the emitter (smoke keeps drifting where it
|
|
was made while the torch moves on); `emitterSetLocal` makes the whole cloud
|
|
follow the node instead. Deleting the node deletes its emitters.
|
|
|
|
[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
|
|
|
|
A particle is a textured quad. Without a texture it is a soft disc the
|
|
engine provides; `emitterSetTexture` gives it a loaded sprite instead (a
|
|
puff, a spark streak, a snowflake), and for an animated sprite
|
|
`emitterSetFrames` picks a random frame per particle, so one atlas of puffs
|
|
gives a varied cloud. Blending is normal (`PARTICLE_ALPHA`) or additive
|
|
(`PARTICLE_ADD`, for anything that glows). `emitterSetColor` gives the
|
|
color and alpha at birth and at death, and the particle fades between them
|
|
over its life; `emitterSetSize` does the same for its size, with a third
|
|
value for how fast it spins.
|
|
|
|
==== Trails, Collisions, Lit and Soft Particles
|
|
|
|
`emitterSetTrail(emitter, length, width)` keeps the last few positions of
|
|
every particle and draws a ribbon through them, fading toward the tail:
|
|
sparks, tracer rounds, comets, in 2D and 3D alike. `emitterSetCollide` lets
|
|
particles bounce: off a horizontal floor at a height you give
|
|
(`COLLIDE_FLOOR`, free), or off the physics world by ray casts
|
|
(`COLLIDE_SCENE`, 3D only, capped per frame so a big emitter spreads the
|
|
cost over a few frames).
|
|
|
|
Two more are for 3D only. `emitterSetLit` shades each particle by the
|
|
scene's lights and sky as a small sphere facing the camera, so smoke takes a
|
|
brazier's color and dust glints in the sun (shadows are not read).
|
|
`emitterSetSoftness` fades a particle out over a distance where it meets
|
|
geometry, so smoke does not cut into a floor or a fountain into its pool;
|
|
this costs one extra depth pass of the scene per frame while any emitter
|
|
uses it.
|
|
|
|
[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) -- Takes the fire's color from below.
|
|
emitterSetSoftness(smoke, 0.4) -- No hard edge where it meets the bowl.
|
|
emitterStart(smoke)
|
|
|
|
embers = emitterNew(brazier)
|
|
emitterSetBlend(embers, PARTICLE_ADD)
|
|
emitterSetTrail(embers, 6, 0.02) -- A short streak behind each ember.
|
|
emitterSetCollide(embers, COLLIDE_FLOOR, 0.3, 0.8, 0) -- Bounce off the ground at Y = 0.
|
|
emitterStart(embers)
|
|
----
|
|
|
|
==== Cost
|
|
|
|
Particles are cheap to simulate and cost what they cover on screen. A few
|
|
thousand small ones are nothing; a handful of large additive ones over the
|
|
whole screen are what slow a Raspberry Pi 4 down. The pool is capped by
|
|
`emitterSetMax` (default 1000); when it is full, no more are born until some
|
|
die.
|
|
|
|
[#authoring]
|
|
=== Describing a Game Instead of Writing One
|
|
|
|
A game can be written as a *description* -- a table of layers, entities and
|
|
rules -- and compiled into an ordinary Singe game by Forge, the authoring
|
|
tool. Forge is distributed on its own, beside the engine, and has its own
|
|
manual: `docs/Forge.adoc` in the source tree, built to `Forge.html` and
|
|
`Forge.pdf` beside this one.
|
|
|
|
|
|
=== Migrating from Singe 2.10
|
|
|
|
Singe 3.00 moved the sprite handle to the first argument of `spriteDraw`,
|
|
`spriteLoop`, `spriteQuality`, `spriteRotate`, `spriteRotateAndScale`,
|
|
`spriteScale`, and `spriteSetFrame`, so every sprite call now matches the
|
|
`video*` family. To update a game, move the last argument of each of those
|
|
calls to the front:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(x, y, cursor) -- 2.10
|
|
spriteDraw(cursor, x, y) -- 3.00
|
|
spriteRotate(angle, cursor) -- 2.10
|
|
spriteRotate(cursor, angle) -- 3.00
|
|
----
|
|
|
|
A game you cannot edit can opt into the old order instead. Either set the
|
|
global before loading the framework:
|
|
|
|
[source,lua]
|
|
----
|
|
SINGE_LEGACY_SPRITE_ARGS = true
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
or add `LEGACY_SPRITE_ARGS = true` to its `games.dat` entry. Nothing else
|
|
changed shape, but a few behaviors did:
|
|
|
|
* Handles and counts are returned as Lua integers; `videoIsPlaying` returns
|
|
a boolean; `mouseSetMode` returns nothing.
|
|
* Errors inside callbacks now end the game with a traceback instead of being
|
|
printed and ignored, matching how argument errors have always behaved.
|
|
* Held keys no longer repeat in `MODE_NORMAL`.
|
|
* Nothing is written beside the game. Without `--datadir` the data
|
|
directory is `data/<game>/` under the game directory, where 2.x used the
|
|
game's own directory; saves, settings and high scores belong under
|
|
`singeGetDataPath()`.
|
|
* `math.randomseed` accepts a fractional seed, as it did under 2.10's Lua;
|
|
it is floored before Lua 5.4's generator sees it. `os.clock` is wall time
|
|
since the engine started, not processor time.
|
|
* The disc is parked on frame 1 by the engine before your script runs;
|
|
`Framework.singe` no longer seeks when it is loaded.
|
|
* `onMouseMoved` in `MANY_MOUSE` mode passes real movement in its third and
|
|
fourth arguments, and `onControllerMoved` passes the controller index
|
|
(`0` to `3`) instead of SDL's instance ID.
|
|
* `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` /
|
|
`singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and
|
|
`singeSetPauseKeyEnabled`.
|
|
* A laserdisc is no longer assumed. A `games.dat` entry with a `VIDEO` is a
|
|
laserdisc game, as before; one without runs on a canvas instead. On the
|
|
command line pass `--disc` (or `--framefile`), since a video found beside
|
|
the script no longer implies a disc. See <<withoutadisc,Games Without a
|
|
Disc>>.
|
|
* The pause key now pauses the whole game, not just the media: the script
|
|
is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered
|
|
to the script only when the key has been disabled. It acts on the key
|
|
press rather than the release. See <<pausing,Pausing>>.
|
|
|
|
=== Engine Constants
|
|
|
|
The engine defines these globals before any script runs, so they are
|
|
available to `controls.cfg` and to `Framework.singe` alike:
|
|
|
|
[cols="2,3",options="header"]
|
|
|===
|
|
| Constant | Meaning
|
|
| `SWITCH_UP` ... `SWITCH_MOUSE_DISCONNECT` | Values passed to `onInputPressed` / `onInputReleased` in `MODE_NORMAL`. `SWITCH_MOUSE_DISCONNECT` is raised by the engine itself when a mouse or light gun is unplugged, once as a press and once as a release; no key or button produces it.
|
|
| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Arguments for `fontQuality`.
|
|
| `MODE_NORMAL`, `MODE_FULL` | Arguments for `keyboardSetMode`.
|
|
| `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`.
|
|
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`.
|
|
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`.
|
|
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Return values of `discGetState`.
|
|
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Arguments for `lightNew`.
|
|
| `BODY_STATIC`, `BODY_DYNAMIC`, `BODY_KINEMATIC` | Body types for `bodyNew`.
|
|
| `SHAPE_BOX`, `SHAPE_SPHERE`, `SHAPE_CAPSULE`, `SHAPE_CYLINDER`, `SHAPE_HULL`, `SHAPE_MESH` | Collision shapes for `bodyNew`.
|
|
| `JOINT_HINGE`, `JOINT_BALL`, `JOINT_SLIDER` | Joint types (the `joint*` calls name them directly).
|
|
| `DEBUG_NONE`, `DEBUG_SHAPES`, `DEBUG_CONSTRAINTS`, `DEBUG_CONTACTS`, `DEBUG_VELOCITIES`, `DEBUG_STATIC`, `DEBUG_ALL` | What `physicsSetDebug` draws; add them together.
|
|
| `PARTICLE_ALPHA`, `PARTICLE_ADD` | Blending for `emitterSetBlend`.
|
|
| `PARTICLE_OVER`, `PARTICLE_UNDER` | Where 2D particles draw, for `emitterSetLayer`.
|
|
| `COLLIDE_NONE`, `COLLIDE_FLOOR`, `COLLIDE_SCENE` | What particles bounce off, for `emitterSetCollide`.
|
|
| `BILLBOARD_NONE`, `BILLBOARD_ALL`, `BILLBOARD_Y` | How a node faces the camera, for `nodeSetBillboard`.
|
|
| `VEHICLE_CAR`, `VEHICLE_MOTORCYCLE`, `VEHICLE_TANK`, `VEHICLE_BOAT` | Kinds of vehicle for `vehicleNew`.
|
|
| `SOFT_CLOTH`, `SOFT_BODY`, `SOFT_ROPE` | Kinds of soft body for `softNew`.
|
|
| `FILTER_LINEAR`, `FILTER_NEAREST` | Texture sampling for `materialSetFilter`.
|
|
| `TONEMAP_NEUTRAL`, `TONEMAP_ACES`, `TONEMAP_NONE` | Tone curves for `sceneSetTonemap`.
|
|
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free.
|
|
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v3.00"`), and as the number `singeVersion()` returns.
|
|
| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`, in raw axis units.
|
|
| `SINGE_TRIGGER_THRESHOLD` | The analogue trigger threshold in those same raw units; `0` when the triggers are on `SINGE_DEAD_ZONE`, which is the default.
|
|
| `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order.
|
|
| `SINGE_DISC` | True when the game has a laserdisc; false when the canvas is the world.
|
|
| `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` | Layout of the controller and mouse input codes, and how many axes and buttons a pad has; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them, and a service screen walks a pad with them.
|
|
|
|
| `SINGE_SCALE_MIN`, `SINGE_SCALE_MAX`, `SINGE_SHIFT_MIN`, `SINGE_SHIFT_MAX`, `SINGE_ROTATE_STEP` | What `vldpSetScale`, `vldpSetShift` and `vldpSetRotate` will take. Clamp to these rather than to your own copies: a shift out of range ends the script.
|
|
|===
|
|
|
|
`Framework.singe` adds the `SCANCODE` and `MODIFIER` tables (SDL's key and
|
|
modifier values), `GAMEPAD_0` to `GAMEPAD_3`, `MOUSE_0` to `MOUSE_3`, the
|
|
`GAMEPAD_AXIS_*` indexes for `controllerGetAxis`, and `DIR`.
|
|
|
|
=== Included Libraries
|
|
|
|
In addition to the standard Lua libraries and the Singe API, the following
|
|
libraries are also available for use in Singe programs without the need to
|
|
install any additional software:
|
|
|
|
[cols="1,3",options="header"]
|
|
|===
|
|
| Library | Purpose
|
|
| Copas | Asynchronous networking
|
|
| LuaFileSystem | Expanded filesystem support
|
|
| json.lua | JavaScript Object Notation encoding and decoding
|
|
| binaryheap | Binary heap data structure
|
|
| timerwheel | Efficient timer scheduling
|
|
| LuaSec | TLS / SSL communication
|
|
| LuaSocket | TCP and UDP
|
|
| LuaRS232 | RS232 serial port access
|
|
| lsqlite3 | SQLite databases, `require("sqlite3")`
|
|
| md5 | `require("md5")` for `sum`, `hexsum`, `exor`, `crypt` and `decrypt`; the PUC-Rio library Hypseus bundles, whose `sumhexa` it names `hexsum`
|
|
| LuaBitOp | `require("bit")` for the bitwise operations of Lua 5.1, which 5.4 has as operators
|
|
| lrandom | `require("random")` for a Mersenne Twister generator, separate from `math.random`
|
|
| Lua CJSON | `require("cjson")` for JSON encoding and decoding, beside the `json` module above
|
|
| LPeg | `require("lpeg")` for parsing expression grammars
|
|
| middleclass | `require("middleclass")` for classes, if you would rather build entities out of them than out of tables
|
|
| lume | `require("lume")` for the small things every game needs: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice`
|
|
| inspect | `require("inspect")` to print a table in a shape a person can read, for when `debugPrint` is the debugger
|
|
| bump | `require("bump")` for moving things that must not pass through each other -- it answers where a mover ends up, which <<collide,the collide calls>> deliberately do not
|
|
| RmlUi | The GUI's own object model, the `rmlui` global (see <<gui,GUI>>)
|
|
|===
|
|
|
|
Their usage is beyond the scope of this document. The last five are there
|
|
because Hypseus exposes them, so a game written against Hypseus finds the same
|
|
modules under the same names; nothing in Singe needs them.
|
|
|
|
[#audiosync]
|
|
=== Audio Sync
|
|
|
|
Singe presents video against the audio clock: a frame is shown when the sound
|
|
that belongs with it is heard. The engine measures its own audio path at
|
|
startup. SDL cannot report how much audio is queued between the mixer and the
|
|
speaker, so Singe drains the device once, before anything audible plays, and
|
|
counts the burst of buffers SDL uses to refill it; that burst is the queue
|
|
depth, and the clock subtracts it. Run with `--program` to see the measurement
|
|
in the trace (`Audio device queue: ...`).
|
|
|
|
What the engine cannot see is anything downstream of the operating system's
|
|
audio server: a DAC, an HDMI receiver, a Bluetooth link. Those add a fixed
|
|
delay that varies by machine. If actions on screen visibly precede their
|
|
sound, pass the missing milliseconds with `--audiodelay=MS`, put `AUDIO_DELAY
|
|
= MS` in `games.dat`, or let the game set it from a service menu with
|
|
<<singesetaudiodelay,singeSetAudioDelay>>. Positive values mean the audio is
|
|
late; the video is held back to match. A negative value handles the rare case
|
|
of audio arriving early.
|
|
|
|
[#menucalibration]
|
|
==== Calibrating from the menu
|
|
|
|
The bundled menu has a calibration screen for that downstream delay. Press the
|
|
key mapped to `INPUT_SERVICE` (the `9` key by default) on the game list and
|
|
choose Audio Delay from the service tools (see <<servicetools,The Service
|
|
Tools>>). The screen is a page of the menu's document: it covers the menu in
|
|
black, shows the delay being tried and the device queue the engine measured,
|
|
plays a click once a second and turns white for one frame at each flash.
|
|
Adjust with left and right (10 ms) and up and down (1 ms) until the flash and
|
|
the click happen together, then press button 1 to save. Start resets to zero
|
|
and button 2 (or the service key again) cancels. The value is stored in
|
|
`machine.cfg` in the data root and applied by the engine to every game on that
|
|
machine, whether launched from the menu or from the command line; it is added
|
|
to any per-game `AUDIO_DELAY`.
|
|
Recalibrate after changing speakers, headphones, or displays.
|
|
|
|
The screen works because the click and the disc audio share one mixer device
|
|
and one queue: the flash is scheduled the measured queue plus the candidate
|
|
value after the click, so when the two coincide the candidate is exactly the
|
|
delay the engine cannot see. Display lag is folded in for free. A game can
|
|
read the values with <<singegetaudiocalibration,singeGetAudioCalibration>> and
|
|
<<singegetaudiolatency,singeGetAudioLatency>>, or offer its own screen with
|
|
<<singesetaudiocalibration,singeSetAudioCalibration>>.
|
|
|
|
Two properties of the video file itself also matter. AAC audio carries encoder
|
|
priming samples that the container must tell decoders to skip; files muxed
|
|
without that information (an MP4 with no edit list, a Matroska file with no
|
|
codec delay) play their audio about 20 milliseconds late, which `AUDIO_DELAY`
|
|
can absorb. And when the first video frame is timestamped later than the first
|
|
audio sample, Singe honours the container's timing, so leading audio is not
|
|
lost.
|
|
|
|
[#formats]
|
|
=== Video, Audio, and Container Formats
|
|
|
|
Singe decodes video with FFmpeg's libraries directly, using the platform's hardware
|
|
decoder when it offers one for the codec (VA-API or VDPAU on Linux, D3D11VA on
|
|
Windows, VideoToolbox on macOS, and on an ARM board the V4L2 decoders -- the
|
|
memory-to-memory one, which on a Pi 4 covers H.264, the stateless Request API
|
|
one a Pi 5 uses for HEVC, and Rockchip's own on an RK3588) and its own software
|
|
decoder otherwise; the program trace reports which, and `--softwarevideo` forces
|
|
software. Any container
|
|
and codec the
|
|
bundled FFmpeg can demux and decode will play: MP4, MKV, MPEG program streams,
|
|
AVI, AV1 and the classic Daphne `.m2v` elementary streams with a matching `.ogg`
|
|
audio file next to them. Every audio track in the file is available to
|
|
`discSetAudioTrack` / `videoSetAudioTrack`; all tracks must share one sample
|
|
format, channel count, and rate.
|
|
|
|
A laserdisc held interlaced fields, and a rip that kept them combs on every
|
|
progressive display. `--deinterlace` decides what happens: `auto`, the default,
|
|
runs bwdif over the frames the file marks interlaced and leaves everything else
|
|
alone, so a progressive disc pays nothing; `on` is for a file whose flags are
|
|
wrong; `off` never touches the picture. One picture comes out for each one that
|
|
goes in -- a disc is addressed by frame number, so a deinterlacer that turned
|
|
each frame into two fields would move every frame in the index.
|
|
|
|
The first time a video is opened, Singe indexes it and stores the index next
|
|
to the game's other data (`<name>-<hash>.index`, the hash made from the
|
|
video's full name so two videos with one base name keep separate indexes). Indexing is one pass over the file
|
|
without decoding, a few seconds even for a feature length disc, and happens
|
|
again if the video changes. When a video is loaded, Singe
|
|
reports its keyframe spacing in the program trace (`--program`), with a
|
|
warning there if keyframes are more than two seconds apart, because a seek
|
|
has to decode forward from the previous keyframe. Decoding happens on a separate thread,
|
|
so a slow seek shows the previous frame a little longer instead of stalling
|
|
the game.
|
|
|
|
A framefile's segments are opened as the disc reaches them, not all at once: the
|
|
one playing, the one after it, and room for a seek to move between two. A long
|
|
framefile can have hundreds of segments -- one released game has 213 -- and
|
|
holding a decoder open for every one of them costs hundreds of open files for no
|
|
gain. A framefile that names a file which is not there still says so at startup.
|
|
|
|
For laserdisc footage the constraints are frame accuracy and seek speed, not
|
|
compression. H.264 in MP4 or MKV with a short keyframe interval (one or two
|
|
seconds) seeks quickly and plays on every supported platform; long keyframe
|
|
intervals make `discSearch` visibly slow because the decoder must walk from
|
|
the previous keyframe. Standard definition (720x480 or 720x576) is the sweet
|
|
spot. High definition sources work but cost proportionally more CPU on
|
|
Raspberry Pi class hardware, and the overlay defaults to half the video
|
|
resolution, so oversized video buys little.
|
|
|
|
Sound effects and music are read by SDL_mixer first and by FFmpeg for anything
|
|
it turns down, so between them they cover:
|
|
|
|
* WAV, AIFF, AU and VOC for uncompressed audio. Short uncompressed WAV files
|
|
give the lowest latency, and are what a gunshot should be.
|
|
* Ogg Vorbis, Opus, MP3, FLAC and WavPack.
|
|
* AAC (`.m4a`, `.aac`), ALAC, AC-3, WMA, APE, TTA, Speex and AMR -- everything
|
|
else the bundled FFmpeg decodes.
|
|
* Tracker modules through libxmp: MOD, S3M, XM and IT, and fifty-odd others
|
|
including 669, AMF, MED, OKT, PTM, STM and ULT.
|
|
* Chiptunes through game-music-emu: NSF and NSFE, SPC, VGM, GBS, AY, GYM, HES,
|
|
KSS and SAP. The gzipped VGZ form is not read; ungzip it to VGM first.
|
|
* MIDI, synthesised from a SoundFont. See <<midi,MIDI>>.
|
|
|
|
The `Audio:` line of the trace header (`--program` or `--trace`) lists the
|
|
decoders the build actually carries.
|
|
|
|
Pictures work the same way: SDL_image first, FFmpeg for the rest. That covers
|
|
PNG, JPEG, WebP, GIF, BMP, TGA, PCX, PNM, LBM, QOI, XCF, XPM and SVG from
|
|
SDL_image, and TIFF, OpenEXR, JPEG 2000, DDS, PSD, DPX, AVIF and HEIC from
|
|
FFmpeg. An animated GIF or WEBP loads as a sprite animation; see
|
|
<<spriteload,`spriteLoad`>>, which also takes a size for rasterising an SVG.
|
|
Radiance `.hdr` and OpenEXR both work as a sky (<<scenesetsky,`sceneSetSky`>>)
|
|
and keep their full range.
|
|
|
|
Fonts are read by FreeType: TrueType, OpenType, WOFF, WOFF2, Type 1, BDF and
|
|
PCF, with colour glyphs (both the bitmap and the vector kind) and HarfBuzz
|
|
shaping for Arabic, Hebrew and the Indic scripts.
|
|
|
|
[#reference]
|
|
== Lua API Reference
|
|
|
|
This chapter documents the Lua API exposed by the Singe engine -- every
|
|
function your game script can call, and every callback Singe will invoke on
|
|
you. It is aimed at developers writing new games or porting video-heavy games
|
|
from other engines.
|
|
|
|
[#conventions]
|
|
=== Conventions
|
|
|
|
Every API entry below follows the same structure:
|
|
|
|
* *Signature(s)* in a code fence, showing every accepted form.
|
|
* *Description* of what the function does and any side effects.
|
|
* *Parameters* listed inline, or as a table when there are more than two.
|
|
* *Returns* line describing the value pushed back to Lua (omitted when the function returns nothing).
|
|
* *Notes*, if there are gotchas worth flagging.
|
|
* *Since* -- the Singe version the function first appeared in.
|
|
* *See also* -- related functions you will likely use in the same call site.
|
|
* *Example* -- a short, realistic snippet in the shape of real game code.
|
|
|
|
An entry that begins *Hypseus extension* is there so that a game written for
|
|
Hypseus Singe runs here unchanged. The same goes for an argument form marked as
|
|
Hypseus's on a function that otherwise takes Singe's own.
|
|
|
|
IMPORTANT: *Do not write new games against the Hypseus API.* Those entries exist
|
|
for compatibility, not as a recommendation. Several of them answer a fixed value
|
|
because the thing they describe does not exist in Singe, some are accepted and
|
|
ignored, and others carry scales and argument orders inherited from another
|
|
engine -- `videoGetVolume()` counts to `64` where the rest of Singe counts to
|
|
`100`, and the `spriteAnim` names take the sprite last where every Singe sprite
|
|
call takes it first. Each entry names the Singe equivalent to use instead. New
|
|
code that sticks to the Singe API is shorter, consistent with itself, and will
|
|
not be surprised when a compatibility answer stays constant.
|
|
|
|
A few rules apply across the whole API:
|
|
|
|
* *Colors* are 8-bit per channel: red, green, blue, and opacity are each integers from `0` to `255`.
|
|
* *Overlay coordinates* are in the current overlay resolution (set by `overlaySetResolution`), not screen pixels. The default overlay resolution is half the video resolution in each dimension; call `overlaySetResolution` to change it.
|
|
* *Handles* returned by `*Load` functions (sprites, sounds, fonts, videos) are opaque integers. Pass them back unchanged to the other functions in that namespace. Do not do math on them.
|
|
* *Invalid arguments abort the script.* If a function is called with the wrong number of arguments, wrong types, or a handle that has already been unloaded, Singe terminates the running script with an error -- it does not return `nil` or a failure code. Validate user input in your script before handing it to the API.
|
|
* *Drawing must happen from `onOverlayUpdate`.* Calls to any `overlay*`, `spriteDraw`, `videoDraw`, or `fontPrint` function from other callbacks will not be reflected on screen and may corrupt the overlay. This is the one rule that catches every new Singe developer.
|
|
|
|
[#animation]
|
|
=== Animation
|
|
|
|
These calls drive the animations a model carries, per instance. Every one
|
|
takes the *root node* returned by `modelInstance`; other nodes raise an
|
|
error, except `animationIsPlaying`, which answers `false`. Animations are
|
|
numbered from `1` in the order `modelGetAnimations` lists them, or named as
|
|
the file names them. A clip plays on the base layer, and up to four more can
|
|
play on layers `1` to `4` above it; the time, pause, resume and seek calls
|
|
address the base layer only. Time is in seconds, advances with the wall
|
|
clock by at most a tenth of a second a frame, and holds while the game is
|
|
paused. Node animation, skinning and
|
|
morph target weights are all driven. See the Animation part of
|
|
<<scenes3d,3D Scenes>>.
|
|
|
|
[#animationgettime]
|
|
==== allowSocketCall
|
|
|
|
[source,text]
|
|
----
|
|
allowed = allowSocketCall()
|
|
----
|
|
|
|
Hypseus extension, always `false` in Singe. Hypseus answers whether the user let it send data to a stats server, which its `-noserversend` turns off; the sending itself is commented out there. Singe sends nothing anywhere, so nothing is permitted and the answer never changes. It exists so a game that asks does not stop on a missing function.
|
|
|
|
*Returns:* a boolean, always `false`.
|
|
|
|
*Since:* 3.00
|
|
|
|
==== animationGetTime
|
|
|
|
[source,text]
|
|
----
|
|
seconds = animationGetTime(node)
|
|
----
|
|
|
|
The current time into the base layer's clip, playing or paused, in the clip's own seconds (a `speed` of `2` moves it twice as fast). A looping clip wraps back to `0` at its end; a non-looping one stops at its length. Nothing played yet gives `0`. Clips on the upper layers are not reported. A node that is not an instance root raises an error.
|
|
|
|
*Returns:* Seconds as a number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationsettime,animationSetTime>>, <<animationisplaying,animationIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Play the footstep once each time the walk cycle passes its two contact points.
|
|
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)
|
|
----
|
|
|
|
Whether the base layer's clip is advancing on this instance. A non-looping clip stops playing when it reaches its end, and `animationPause` and `animationStop` both make this `false`; a looping clip stays `true` until stopped. Unlike the other calls here, a node that is not an instance root does not raise an error but answers `false`.
|
|
|
|
*Returns:* Boolean.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationplay,animationPlay>>, <<animationpause,animationPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Back to idling once the one-shot attack has run out.
|
|
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)
|
|
----
|
|
|
|
Freezes the base layer's clip at its current time; the nodes hold that pose until `animationResume` continues it or another clip is played. Clips on the upper layers keep running. Pausing an instance that has nothing playing is harmless. A node that is not an instance root raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationresume,animationResume>>, <<animationsettime,animationSetTime>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A cutscene freezes the crowd while the camera moves.
|
|
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)
|
|
----
|
|
|
|
Starts one of the model's clips from its beginning on the base layer of this instance, replacing whatever was playing there; the pose updates at once. With a `fade` the old clip keeps running while it fades out over that many seconds, and when nothing was playing the fade starts from the pose the nodes are in now (a ragdoll that was just switched off, a scripted pose), with nodes the new clip does not animate easing back to the model's rest pose. A node that is not an instance root, a name the model does not have, or a number outside `1` to the clip count raises an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the instance's root node from `modelInstance`.
|
|
* `nameOrIndex` -- a clip name as the file stores it, or its number from `1` in `modelGetAnimations` order.
|
|
* `loop` -- boolean, default `true`. When `false` the clip stops at its last keyframe and `animationIsPlaying` becomes `false`.
|
|
* `speed` -- playback rate, default `1`. `2` is twice as fast; `0.5` is half.
|
|
* `fade` -- seconds to crossfade from what was playing, default `0` (a snap).
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<modelgetanimations,modelGetAnimations>>, <<animationplaylayer,animationPlayLayer>>, <<animationstop,animationStop>>, <<animationsettime,animationSetTime>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A guard that idles until the player is spotted, then runs.
|
|
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)
|
|
----
|
|
|
|
Plays a clip on one of the four layers above the base, blended over whatever is beneath it by the layer's weight (`animationSetLayerWeight`, default `1`) and limited to part of the skeleton when a mask is set (`animationSetLayerMask`). Higher layers blend over lower ones; each layer runs its own clip, time and loop independently of the base. `nameOrIndex`, `loop`, `speed` and `fade` mean what they do in `animationPlay`, with one difference: a `fade` on a layer that was empty ramps the layer's weight in from `0` instead of crossfading from a pose. A layer outside `1` to `4`, a node that is not an instance root, or a clip the model does not have raises an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the instance's root node.
|
|
* `layer` -- `1` to `4`.
|
|
* `nameOrIndex` -- a clip name or number, as in `animationPlay`.
|
|
* `loop` -- default `true`.
|
|
* `speed` -- default `1`.
|
|
* `fade` -- seconds, default `0`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationplay,animationPlay>>, <<animationsetlayermask,animationSetLayerMask>>, <<animationsetlayerweight,animationSetLayerWeight>>, <<animationstop,animationStop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Walk with the whole body, look about with the head only.
|
|
animationPlay(fox, "Walk", true)
|
|
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5)
|
|
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox))
|
|
|
|
-- Later, wave with one arm over whatever the legs are doing.
|
|
function onGreet()
|
|
animationPlayLayer(fox, 2, "Wave", false, 1, 0.2)
|
|
end
|
|
----
|
|
|
|
[#animationresume]
|
|
==== animationResume
|
|
|
|
[source,text]
|
|
----
|
|
animationResume(node)
|
|
----
|
|
|
|
Continues the base layer's clip from where `animationPause` froze it, or from wherever `animationSetTime` moved it meanwhile. Resuming a clip that is already playing changes nothing. An instance with no clip on its base layer (never played, or stopped) raises an error, as does a node that is not an instance root.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationpause,animationPause>>, <<animationsettime,animationSetTime>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Limits a layer to `maskNode` and everything under it, so the clip on that layer moves only that part of the model and the rest keeps following the layers beneath. `maskNode` must be one of the instance's own nodes, found with `nodeFind(name, node)`; the instance root itself does not count. Leaving `maskNode` out, or passing `nil`, lifts the limit. The mask stays on the layer across later `animationPlayLayer` calls. A layer outside `1` to `4` raises an error, and so does a node that is not an instance root or a `maskNode` that is not part of that instance.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the instance's root node.
|
|
* `layer` -- `1` to `4`.
|
|
* `maskNode` -- a joint or node of the instance; omitted or `nil` for the whole model.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationplaylayer,animationPlayLayer>>, <<nodefind,nodeFind>>, <<animationsetlayerweight,animationSetLayerWeight>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Aim the upper body at the target while the legs keep walking.
|
|
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)
|
|
----
|
|
|
|
How much a layer's clip shows over what is beneath it, `0` (not at all) to `1` (fully, the default); values outside that range are clamped. The weight belongs to the layer, not the clip, so it survives `animationPlayLayer` and `animationStop` on that layer. Change it a little every frame to fade a layer in or out by hand, or let a `fade` on `animationPlayLayer` do it once. A layer outside `1` to `4` or a node that is not an instance root raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationplaylayer,animationPlayLayer>>, <<animationsetlayermask,animationSetLayerMask>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The limp gets worse as health drops.
|
|
function onHealthChanged(health)
|
|
local limp = 1 - health / maxHealth
|
|
animationSetLayerWeight(hero, 1, limp)
|
|
end
|
|
----
|
|
|
|
[#animationsettime]
|
|
==== animationSetTime
|
|
|
|
[source,text]
|
|
----
|
|
animationSetTime(node, seconds)
|
|
----
|
|
|
|
Jumps the base layer's clip to a time in its own seconds; negative values become `0`. The pose updates at once whether or not the clip is playing, so a paused instance can be scrubbed to an exact frame, and a playing one continues from the new time. Any crossfade in progress on the base layer is cut short. An instance with no clip on its base layer raises an error, as does a node that is not an instance root.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationgettime,animationGetTime>>, <<animationpause,animationPause>>, <<animationresume,animationResume>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A door whose opening clip follows a lever the player drags.
|
|
animationPlay(door, "Open", false)
|
|
animationPause(door)
|
|
|
|
function onLeverMoved(fraction)
|
|
animationSetTime(door, fraction * doorClipLength)
|
|
end
|
|
----
|
|
|
|
[#animationstop]
|
|
==== animationStop
|
|
|
|
[source,text]
|
|
----
|
|
animationStop(node)
|
|
animationStop(node, layer)
|
|
----
|
|
|
|
Stops and forgets every clip on the instance, or only the clip on one layer above the base (`1` to `4`); the base layer alone cannot be stopped without the layers above it. The nodes keep their last pose, which a following `animationPlay` with a `fade` eases out of. After stopping everything, `animationResume` and `animationSetTime` raise an error until something is played again. Layer weights and masks are kept. A layer outside `1` to `4` or a node that is not an instance root raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationplay,animationPlay>>, <<animationplaylayer,animationPlayLayer>>, <<animationpause,animationPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hand the body to the physics ragdoll: freeze the pose, then let it fall.
|
|
function onKilled()
|
|
animationStop(hero)
|
|
ragdollActivate(hero)
|
|
end
|
|
|
|
-- The wave on layer 2 is done; the walk beneath it carries on.
|
|
function onWaveFinished()
|
|
animationStop(hero, 2)
|
|
end
|
|
----
|
|
|
|
[#bezelref]
|
|
=== Bezel
|
|
|
|
Cabinet artwork and the arcade scoreboard that goes with it. `mainBezelLoaded`
|
|
asks about the artwork `--bezel` loaded; the `scoreBezel` family drives the score
|
|
panel, an RmlUi document the engine ships and either a theme or a game may
|
|
restyle. Every call here is a Hypseus Singe extension, kept so its games run
|
|
unchanged, and every one of them is described from the player's side in
|
|
<<bezels,Bezels>>.
|
|
|
|
The panel starts switched off, as it does in Hypseus, and only a game turns it
|
|
on. Values given to it are remembered whether it is showing or not, so a game
|
|
may set them before it enables the panel. Each readout has the number of digits
|
|
Hypseus's scoreboard has -- six for a score, two for the credits, one for a life
|
|
count -- and a value is shown right justified in them with leading zeros; a
|
|
score of more than six digits shows its last six. Hypseus's negative values are
|
|
understood too: `-2` lights a dash in every digit, `-4` an `H` and `-5` an `A`,
|
|
and `-1`, `-3` and any other negative number light nothing.
|
|
|
|
[#mainbezelloaded]
|
|
==== mainBezelLoaded
|
|
|
|
[source,text]
|
|
----
|
|
loaded, id = mainBezelLoaded()
|
|
----
|
|
|
|
Hypseus extension. Whether `--bezel` loaded cabinet artwork, and the identifier
|
|
of the file it loaded. The identifier is the 64 bit FNV-1a hash of the file's
|
|
bytes written out as a decimal string, which is what Hypseus reports, so a game
|
|
that recognises a particular bezel by its number recognises the same file here.
|
|
With no artwork loaded the answers are `false` and `"0"`.
|
|
|
|
*Returns:* boolean, and the identifier as a string.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<setoverlayontop,setOverlayOnTop>>, <<bezels,Bezels>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Only draw the sprite frame when the player has no artwork of their own.
|
|
local loaded, id = mainBezelLoaded()
|
|
if not loaded then
|
|
spriteDraw(frameSprite, 0, 0)
|
|
end
|
|
----
|
|
|
|
[#scorebezelclear]
|
|
==== scoreBezelClear
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelClear()
|
|
----
|
|
|
|
Hypseus extension. Blanks every readout on the score panel: the credits, both
|
|
scores and both life counts show nothing at all, leaving only the dim digits
|
|
behind them. The values are not forgotten, but the next `scoreBezelCredits`,
|
|
`scoreBezelScore` or `scoreBezelLives` lights the panel again with whatever it is
|
|
then holding. Safe to call with the panel switched off.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezelenable,scoreBezelEnable>>
|
|
|
|
[#scorebezelcredits]
|
|
==== scoreBezelCredits
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelCredits(credits)
|
|
----
|
|
|
|
Hypseus extension. Sets the credit readout, which has two digits. Also lights a
|
|
panel that `scoreBezelClear` blanked.
|
|
|
|
*Parameters:*
|
|
|
|
* `credits` -- integer; the last two digits are shown.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezelscore,scoreBezelScore>>
|
|
|
|
[#scorebezelenable]
|
|
==== scoreBezelEnable
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelEnable(enabled)
|
|
scoreBezelEnable(enabled, type)
|
|
----
|
|
|
|
Hypseus extension. Shows or hides the score panel. The first `true` builds it
|
|
from `Singe/scoreBezel.rml`; hiding and showing it again after that costs
|
|
nothing and keeps its values. It starts hidden, which is how Hypseus ships as
|
|
well, so a game that never calls this behaves exactly as it did before -- and
|
|
should ask <<scorebezelgetstate,`scoreBezelGetState`>> before assuming it has one.
|
|
|
|
Hypseus's `type` chooses among its five scoreboard backends. Singe has one, an
|
|
RmlUi document, so the argument is checked and ignored. A machine with no GPU has
|
|
no GUIs at all; there the panel cannot be built, `scoreBezelGetState` keeps
|
|
answering `false`, and a warning is printed once. A non-boolean first argument
|
|
aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
* `type` -- integer, accepted and ignored.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezelgetstate,scoreBezelGetState>>, <<bezels,Bezels>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The engine's panel when there is one, sprites when there is not.
|
|
scoreBezelEnable(true, 0)
|
|
if scoreBezelGetState() then
|
|
scoreBezelTwinScoreOn(twoPlayer)
|
|
scoreBezelCredits(credits)
|
|
else
|
|
drawSpriteScoreboard()
|
|
end
|
|
----
|
|
|
|
[#scorebezelgetstate]
|
|
==== scoreBezelGetState
|
|
|
|
[source,text]
|
|
----
|
|
state = scoreBezelGetState()
|
|
----
|
|
|
|
Hypseus extension. Whether the score panel is showing. Every Hypseus game asks
|
|
this before using the panel and draws its own scoreboard with sprites when the
|
|
answer is `false`, which is the right fallback and not a failure.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezelenable,scoreBezelEnable>>
|
|
|
|
[#scorebezellives]
|
|
==== scoreBezelLives
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelLives(player, lives)
|
|
----
|
|
|
|
Hypseus extension. Sets a player's life readout, which has one digit. `player` is
|
|
`1` or `2`; any other number is ignored, and so is `2` until
|
|
<<scorebezeltwinscoreon,`scoreBezelTwinScoreOn(true)`>> has put the second
|
|
player's readouts on the panel. Also lights a panel that `scoreBezelClear`
|
|
blanked.
|
|
|
|
*Parameters:*
|
|
|
|
* `player` -- `1` or `2`.
|
|
* `lives` -- integer; the last digit is shown.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezelscore,scoreBezelScore>>
|
|
|
|
[#scorebezelscore]
|
|
==== scoreBezelScore
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelScore(player, score)
|
|
----
|
|
|
|
Hypseus extension. Sets a player's score readout, which has six digits. `player`
|
|
is `1` or `2`, with the same rule for `2` as `scoreBezelLives`. Also lights a
|
|
panel that `scoreBezelClear` blanked.
|
|
|
|
*Parameters:*
|
|
|
|
* `player` -- `1` or `2`.
|
|
* `score` -- integer; the last six digits are shown.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezellives,scoreBezelLives>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function addScore(points)
|
|
score = score + points
|
|
if scoreBezelGetState() then
|
|
scoreBezelScore(1, score)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#scorebezeltwinscoreon]
|
|
==== scoreBezelTwinScoreOn
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelTwinScoreOn(enabled)
|
|
----
|
|
|
|
Hypseus extension. Puts the second player's score and life readouts on the panel,
|
|
or takes them off again and leaves the room to the other two. Switching them off
|
|
also blanks the panel, exactly as it does in Hypseus, so follow it with a value
|
|
if the first player's readouts should stay lit. While they are off,
|
|
`scoreBezelScore(2, ...)` and `scoreBezelLives(2, ...)` do nothing. A non-boolean
|
|
argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<scorebezelscore,scoreBezelScore>>
|
|
|
|
[#body]
|
|
=== Body
|
|
|
|
A body gives a scene node a place in the physics world. There is one body per node, addressed by the node handle: a static body stays where the node was when the body was made, a kinematic body follows its node each step and pushes without being pushed, and a dynamic body is moved by the simulation and drives its node. Sizes, positions and velocities are in world units (meters in a 3D scene, overlay pixels in a 2D world), masses in kilograms, and time in seconds. Every call here raises an error when the node has no body; a call that only makes sense for one kind of body (a force on a static body, say) is ignored on the others, except `bodySetMass`, which raises. See <<physics,Physics>> in Game Development for how bodies, nodes and the frame fit together.
|
|
|
|
[#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)
|
|
----
|
|
|
|
Gives the node a body of the given type, placed where the node is in the world right now (its parents included), with a collision shape sized by `a`, `b` and `c` in world units and scaled by the node's world scale. A second call on the same node replaces the first, and a node that had a player loses it. A dynamic body's mass comes from its shape's volume at the density of water, so set `bodySetMass` on anything meant to feel light. Changing the node's scale later does not resize the body. Raises an error for an unknown type or shape, when physics is not available on this machine, when the world already holds `4096` bodies, or when the shape cannot be built (a dynamic `SHAPE_MESH`, or a hull with no mesh under the node).
|
|
|
|
*Parameters:*
|
|
|
|
* `type` -- `BODY_STATIC`, `BODY_DYNAMIC` or `BODY_KINEMATIC`.
|
|
* `shape` -- `SHAPE_BOX` (`a`, `b`, `c` are width, height and depth), `SHAPE_SPHERE` (`a` is the radius), `SHAPE_CAPSULE` or `SHAPE_CYLINDER` (`a` is the radius, `b` the total height along Y, caps included), `SHAPE_HULL` (a convex hull of the node's mesh and everything under it, no sizes), or `SHAPE_MESH` (the exact triangles, static and kinematic bodies only; a heightmap mesh becomes a height field).
|
|
* `a`, `b`, `c` -- the sizes the shape needs; missing ones are `0` and any size is raised to at least `0.001`.
|
|
|
|
*Notes:* A sphere is scaled by the node's largest scale axis, a capsule or cylinder radius by the larger of X and Z. A capsule shorter than twice its radius is a sphere with a sliver in the middle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetmass,bodySetMass>>, <<bodydelete,bodyDelete>>, <<physicssetgravity,physicsSetGravity>>, <<playernew,playerNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A light crate and a duck model as a hull, dropped onto the floor.
|
|
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)
|
|
----
|
|
|
|
Adds a force, in newtons, to a dynamic body for the steps of this frame only; call it every frame to keep pushing. Without a point the force acts through the center of mass and only moves the body; with a world point `(px, py, pz)` it also spins the body about its center. A force accelerates by force divided by mass, so size it to `bodySetMass`. Static and kinematic bodies ignore it.
|
|
|
|
*Parameters:*
|
|
|
|
* `fx`, `fy`, `fz` -- the force in newtons along each world axis.
|
|
* `px`, `py`, `pz` -- optional world point the force acts at; omit for the center of mass.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetmass,bodySetMass>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A fan blows the balloon across the room while the switch is on.
|
|
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)
|
|
----
|
|
|
|
Changes a dynamic body's momentum at once: an impulse of mass times speed adds that speed. Without a point it acts through the center of mass; with a world point `(px, py, pz)` it also spins the body, so hitting a crate near its top tips it over. This is the call for a hit, a kick, an explosion or a launch, where `bodyApplyForce` is for a push that lasts. Static and kinematic bodies ignore it.
|
|
|
|
*Parameters:*
|
|
|
|
* `ix`, `iy`, `iz` -- the impulse in kilogram meters per second along each world axis.
|
|
* `px`, `py`, `pz` -- optional world point the impulse acts at; omit for the center of mass.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodyapplyforce,bodyApplyForce>>, <<bodysetvelocity,bodySetVelocity>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Flick whatever the mouse is over straight up, at the point it was hit.
|
|
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)
|
|
----
|
|
|
|
Removes the body from the world; the node and its mesh stay where they are. Every joint attached to the body, and any vehicle built on it, goes with it. Deleting the node removes its body too, so this is only needed to keep a node without its body. Raises an error when the node has no body.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodynew,bodyNew>>, <<bodysetenabled,bodySetEnabled>>, <<jointdelete,jointDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A collected coin stops being solid and floats up as a plain node.
|
|
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)
|
|
----
|
|
|
|
How fast the body is spinning about each world axis, in radians per second, as of the last step. A static body reports zero. Useful for a wheel's sound pitch or a spin counter.
|
|
|
|
*Returns:* Three numbers, radians per second about X, Y and Z.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetangularvelocity,bodySetAngularVelocity>>, <<bodygetvelocity,bodyGetVelocity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The roulette wheel has stopped when it barely turns.
|
|
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)
|
|
----
|
|
|
|
How fast the body is moving along each world axis, in world units per second, as of the last step. A static body reports zero; a kinematic body reports the velocity its node's movement implied.
|
|
|
|
*Returns:* Three numbers, units per second along X, Y and Z.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetvelocity,bodySetVelocity>>, <<bodygetangularvelocity,bodyGetAngularVelocity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Play a whoosh when the ball is really moving.
|
|
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)
|
|
----
|
|
|
|
Whether the simulation has put the body to sleep because it stopped moving. Static bodies and bodies taken out of the world with `bodySetEnabled` count as resting. A resting body wakes on its own when something touches it or when a velocity, force or impulse is applied.
|
|
|
|
*Returns:* Boolean.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetenabled,bodySetEnabled>>, <<bodysetvelocity,bodySetVelocity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The round ends once every pin has settled.
|
|
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)
|
|
----
|
|
|
|
Sets the spin about each world axis, in radians per second, and wakes the body. Static bodies ignore it. A kinematic body's spin is overwritten from its node at the next step, so the call only lasts for dynamic bodies.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodygetangularvelocity,bodyGetAngularVelocity>>, <<bodysetvelocity,bodySetVelocity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Toss the coin: up, and spinning end over end.
|
|
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)
|
|
----
|
|
|
|
Sets the body's restitution, from `0` (stops dead) to `1` (bounces back with everything it arrived with); values outside that range are clamped. The default is `0.1`. The two touching bodies' values combine, so a lively ball on a dead floor still bounces.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetfriction,bodySetFriction>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A rubber ball dropped onto the scene.
|
|
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)
|
|
----
|
|
|
|
How the body floats when it is inside a water volume: `1` is neutral, more floats higher, less sinks, and `0` is a stone. The default is `1.2`, which floats gently. The factor multiplies the water's own density from `bodySetWater`, and negative values are clamped to `0`. Only dynamic bodies are moved by water, but the value may be set on any body.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetwater,bodySetWater>>, <<bodysetcurrent,bodySetCurrent>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A crate that rides high, an anchor that goes down, a ball that bobs.
|
|
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)
|
|
----
|
|
|
|
Sets the flow inside a water volume, in world units per second along each world axis; dynamic bodies floating in it drift with the flow and a swimming player is carried by it. The default is no flow. Set it on the node that `bodySetWater` was called on; on any other body it is stored and does nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A river that carries anything dropped in it downstream.
|
|
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)
|
|
----
|
|
|
|
Takes the body out of the world with `false`: it stops colliding, stops moving, no longer drives or follows its node, and counts as resting. `true` puts it back where it is, awake. The body keeps its shape, mass and settings, so this is cheaper than deleting and remaking it for something that comes and goes.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodyisresting,bodyIsResting>>, <<bodydelete,bodyDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The drawbridge is solid only while it is down.
|
|
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)
|
|
----
|
|
|
|
Sets the surface friction: `0` is ice, `0.5` (the default) is ordinary, higher values grip more. Negative values are clamped to `0`. The two touching bodies' values combine, so a slippery floor needs a low value on the floor, not on everything that walks on it.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetbounce,bodySetBounce>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A frozen pond in the middle of the 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)
|
|
----
|
|
|
|
Rescales a dynamic body's mass and, with it, its inertia. Do this for anything meant to feel light or heavy: the default is the shape's volume at the density of water, so a door-sized box weighs about 140 kilograms and a nudge will not move it. Forces and impulses scale with mass, so set the mass before tuning them. Raises an error when the body is not dynamic or the mass is not positive.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodynew,bodyNew>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<playersetpush,playerSetPush>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A crate the hero can shove and an anvil nobody can.
|
|
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)
|
|
----
|
|
|
|
Makes the body a trigger with `true`: it pushes nothing and stops nothing, but `onTrigger` reports each body or player that enters and leaves it. `false` makes it solid again: whatever was inside it is forgotten without a leave, while a trigger the body itself stands in goes on reporting it. A trigger is usually a static box or sphere placed over a doorway, a finish line or a danger zone. Rays from `physicsRaycast` hit triggers like any other body.
|
|
|
|
*Notes:* A player is reported inside a trigger when their bounding boxes overlap, so a round trigger is a square one for players.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>, <<bodysetwater,bodySetWater>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A finish line across the track.
|
|
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)
|
|
----
|
|
|
|
Sets the body's velocity along each world axis, in world units per second, and wakes it. This replaces whatever speed the body had, where `bodyApplyImpulse` adds to it. Static bodies ignore it, and a kinematic body's velocity is overwritten from its node's movement at the next step.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodygetvelocity,bodyGetVelocity>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetangularvelocity,bodySetAngularVelocity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Serve the ball toward whichever side lost the last point.
|
|
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)
|
|
----
|
|
|
|
Fills a static body with water and makes it a trigger: the top of its shape is the surface, and every dynamic body inside floats, sinks or rolls by buoyancy against that surface (the submerged part of the shape counts, so a tilted crate rights itself), slowed by the drags. A player whose middle is below the surface swims. Because the volume is a trigger, `onTrigger` fires on the way in and out. The water is not drawn: give the node a blended material, or none. Raises an error when the body is not static.
|
|
|
|
*Parameters:*
|
|
|
|
* `density` -- a multiplier, `1` for water; each body's own `bodySetBuoyancy` scales it. Negative values are clamped to `0`.
|
|
* `linearDrag` -- how quickly the water slows movement through it; default `0.5`.
|
|
* `angularDrag` -- how quickly it slows spinning; default `0.1`.
|
|
|
|
*Notes:* Players and boats see at most eight water volumes; more than that still float bodies but are not swum in.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysetbuoyancy,bodySetBuoyancy>>, <<bodysetcurrent,bodySetCurrent>>, <<bodysettrigger,bodySetTrigger>>, <<playerisswimming,playerIsSwimming>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A pool cut into the deck; its top, at Y = 0, is the surface.
|
|
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]
|
|
=== Camera
|
|
|
|
The camera is a node like any other: place it with `nodeSetPosition`, aim it with `nodeLookAt`, parent it to a moving object for a chase view, and name it with `cameraSet`. It looks down its own -Z. Without a camera node the scene is viewed from `(0, 0, 5)` looking at the origin. The projection is set separately and applies to whichever node is the camera: perspective with a 60 degree vertical field of view from `0.1` to `1000` units by default, or orthographic with `cameraSetOrthographic`. Rendered views (`viewNew`) share the projection but look from their own nodes. See <<scenes3d,3D Scenes>>.
|
|
|
|
[#cameraset]
|
|
==== cameraSet
|
|
|
|
[source,text]
|
|
----
|
|
cameraSet(node)
|
|
cameraSet(-1)
|
|
----
|
|
|
|
Makes a node the camera: the scene is drawn from its world position looking down its own -Z, so its parents' movement and rotation count. Any node will do, including one that also carries a mesh or a light, and switching cameras is one call. `-1` restores the default view from `(0, 0, 5)` looking at the origin. Deleting the camera node also falls back to the default view. Raises an error for a node handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodelookat,nodeLookAt>>, <<camerasetperspective,cameraSetPerspective>>, <<viewsetcamera,viewSetCamera>>, <<nodenew,nodeNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A chase camera behind the player's car, and a fixed one for the replay.
|
|
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)
|
|
----
|
|
|
|
Switches to a parallel projection showing `height` world units top to bottom, the width following the layer's aspect ratio, from `near` to `far` in front of the camera. Distance does not shrink things, which suits diagrams, isometric and side-on views, and HUD-like scenes. The setting applies to the current and any later camera node until `cameraSetPerspective` is called. A directional light's shadow uses a single map under an orthographic camera, whatever `sceneSetShadowCascades` says.
|
|
|
|
*Parameters:*
|
|
|
|
* `height` -- the visible height, in world units.
|
|
* `near` -- the closest distance drawn, in world units in front of the camera.
|
|
* `far` -- the farthest distance drawn.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<camerasetperspective,cameraSetPerspective>>, <<cameraset,cameraSet>>, <<scenesetshadowcascades,sceneSetShadowCascades>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- An isometric view of the board: 12 units tall, from high up and to the side.
|
|
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)
|
|
----
|
|
|
|
Switches to the usual perspective projection, the default. `fov` is the vertical field of view in degrees (default `60`); `near` and `far` (defaults `0.1` and `1000`) bound what is drawn, in world units in front of the camera. Depth precision is spread between the two, so keep `near` as large as the scene allows and `far` no larger than needed: `0.05` to `100` for a room, `0.1` to `400` for a landscape. The setting applies to the current and any later camera node, and to rendered views.
|
|
|
|
*Parameters:*
|
|
|
|
* `fov` -- vertical field of view in degrees; a wide `70` feels fast, a narrow `40` feels like a long lens.
|
|
* `near` -- the closest distance drawn.
|
|
* `far` -- the farthest distance drawn.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<camerasetorthographic,cameraSetOrthographic>>, <<cameraset,cameraSet>>, <<sceneproject,sceneProject>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A tighter lens for the cockpit view, the default for the outside.
|
|
function setView(inside)
|
|
if inside then
|
|
cameraSet(cockpitCamera)
|
|
cameraSetPerspective(45, 0.05, 300)
|
|
else
|
|
cameraSet(chaseCamera)
|
|
cameraSetPerspective(60, 0.1, 400)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#color]
|
|
[#collide]
|
|
=== Collide
|
|
|
|
Whether two flat things touch. Jolt answers this in three dimensions for bodies
|
|
(see <<body,Body>>); these answer it for the overlay, which is where a light
|
|
gun's hitboxes and a 2D game's everything live.
|
|
|
|
They are plain functions: no handles, no state, nothing to create or free, and
|
|
nothing to update each frame. Coordinates are whatever you are already drawing
|
|
in -- overlay pixels, usually. A rectangle is a corner and a size, the way every
|
|
drawing call in the engine takes one. Touching exactly at an edge counts as
|
|
touching, and a rectangle with no width or height touches nothing at all.
|
|
|
|
None of these move anything. When you want to know not just *whether* two things
|
|
met but *where the mover ends up* -- sliding along a wall rather than stopping
|
|
dead in it -- `require("bump")` and let it do that; it is bundled for exactly
|
|
that reason.
|
|
|
|
==== collideCircles
|
|
|
|
[source,text]
|
|
----
|
|
touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
|
|
----
|
|
|
|
Whether two circles overlap. Cheaper than it looks: nothing takes a square root to answer a yes or no question.
|
|
|
|
*Parameters:*
|
|
|
|
* `x1`, `y1`, `radius1` -- numbers; the first circle.
|
|
* `x2`, `y2`, `radius2` -- numbers; the second.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<colliderectcircle,collideRectCircle>>
|
|
|
|
==== collidePointCircle
|
|
|
|
[source,text]
|
|
----
|
|
inside = collidePointCircle(pointX, pointY, x, y, radius)
|
|
----
|
|
|
|
Whether a point falls within a circle. The edge counts as inside.
|
|
|
|
*Parameters:*
|
|
|
|
* `pointX`, `pointY` -- numbers; the point.
|
|
* `x`, `y`, `radius` -- numbers; the circle.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== collidePointPolygon
|
|
|
|
[source,text]
|
|
----
|
|
inside = collidePointPolygon(pointX, pointY, points)
|
|
----
|
|
|
|
Whether a point falls within a polygon of any shape, concave ones included. The table is a flat list of `x`, `y`, `x`, `y` -- the shape a hitbox already has in most games -- and the winding does not matter. Fewer than three corners contains nothing. At most 256 corners.
|
|
|
|
*Parameters:*
|
|
|
|
* `pointX`, `pointY` -- numbers; the point.
|
|
* `points` -- table; an even number of numbers, x and y in turn.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Did the shot land on the dragon, whose outline is not a rectangle?
|
|
local dragon = { 120,40, 200,60, 210,140, 150,180, 100,120 }
|
|
|
|
function onInputPressed(key)
|
|
if key == SWITCH_BUTTON1 then
|
|
local x, y = mouseGetPosition()
|
|
if collidePointPolygon(x, y, dragon) then
|
|
hit()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
==== collidePointRect
|
|
|
|
[source,text]
|
|
----
|
|
inside = collidePointRect(pointX, pointY, x, y, width, height)
|
|
----
|
|
|
|
Whether a point falls within a rectangle. The edge counts as inside.
|
|
|
|
*Parameters:*
|
|
|
|
* `pointX`, `pointY` -- numbers; the point.
|
|
* `x`, `y`, `width`, `height` -- numbers; the rectangle.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A hitbox the size of the sprite it belongs to.
|
|
if collidePointRect(mouseGetPosition()) then end -- (needs the rectangle too; see below)
|
|
|
|
local x, y = mouseGetPosition()
|
|
if collidePointRect(x, y, enemyX, enemyY, spriteGetWidth(enemy), spriteGetHeight(enemy)) then
|
|
kill(enemy)
|
|
end
|
|
----
|
|
|
|
==== collideRectCircle
|
|
|
|
[source,text]
|
|
----
|
|
touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
|
|
----
|
|
|
|
Whether a rectangle and a circle overlap, by finding the rectangle's nearest point to the circle's centre.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y`, `width`, `height` -- numbers; the rectangle.
|
|
* `circleX`, `circleY`, `radius` -- numbers; the circle.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== collideRects
|
|
|
|
[source,text]
|
|
----
|
|
touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
|
|
----
|
|
|
|
Whether two rectangles overlap. The one you will reach for most.
|
|
|
|
*Parameters:*
|
|
|
|
* `x1`, `y1`, `width1`, `height1` -- numbers; the first rectangle.
|
|
* `x2`, `y2`, `width2`, `height2` -- numbers; the second.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<collidepointrect,collidePointRect>>
|
|
|
|
==== collideSegments
|
|
|
|
[source,text]
|
|
----
|
|
crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
|
|
----
|
|
|
|
Whether two line segments cross: a shot along a path against a wall, or the step a mover wants to take against the edge it must not pass. Segments lying along one another answer `false`, because "where do they cross" has no single answer then.
|
|
|
|
*Parameters:*
|
|
|
|
* `ax1`, `ay1`, `ax2`, `ay2` -- numbers; the first segment's ends.
|
|
* `bx1`, `by1`, `bx2`, `by2` -- numbers; the second's.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
=== Color
|
|
|
|
The foreground and background colors are engine globals rather than per-call arguments: set them with `colorForeground` and `colorBackground`, and every later drawing call reads them until they change. The foreground color is used by the `overlay*` shape primitives and by TrueType text from `fontPrint` and `fontToSprite`; the background color is used by `overlayClear` and by `FONT_QUALITY_SHADED` text. `overlayPrint` uses neither. Channels are integers from `0` to `255`, clamped into that range, and both colors accept an optional alpha. See the <<overlay,Overlay>> and <<font,Font>> sections for the calls that consume them.
|
|
|
|
[#colorbackground]
|
|
==== colorBackground
|
|
|
|
[source,text]
|
|
----
|
|
colorBackground(r, g, b)
|
|
colorBackground(r, g, b, a)
|
|
----
|
|
|
|
Sets the background color: what `overlayClear` fills the overlay with, and what `FONT_QUALITY_SHADED` text from `fontPrint` and `fontToSprite` paints behind its glyphs. At startup it is transparent black, and alpha defaults to `0` when omitted, so `colorBackground(0, 0, 0)` followed by `overlayClear()` makes the overlay invisible again. A partial alpha over black dims the video; `255` hides it. Out-of-range channel values are clamped rather than rejected.
|
|
|
|
*Parameters:*
|
|
|
|
* `r`, `g`, `b` -- red, green, and blue, `0` to `255`.
|
|
* `a` -- opacity, `0` (transparent) to `255` (opaque); default `0`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>, <<fontquality,fontQuality>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Solid black behind the calibration screen, transparent during play.
|
|
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)
|
|
----
|
|
|
|
Sets the foreground color used by `overlayBox`, `overlayCircle`, `overlayEllipse`, `overlayLine`, and `overlayPlot`, and the text color for `fontPrint` and `fontToSprite`. At startup it is opaque white, and alpha defaults to `255` when omitted, the opposite of `colorBackground`. Sprites and `overlayPrint` are drawn with their own pixels and are not tinted. Out-of-range channel values are clamped rather than rejected.
|
|
|
|
*Parameters:*
|
|
|
|
* `r`, `g`, `b` -- red, green, and blue, `0` to `255`.
|
|
* `a` -- opacity, `0` (transparent) to `255` (opaque); default `255`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<colorbackground,colorBackground>>, <<overlaybox,overlayBox>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Score in white, low-health warning in red, both from the same font.
|
|
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
|
|
----
|
|
|
|
[#controller]
|
|
[#commandlineref]
|
|
=== Command Line
|
|
|
|
Three calls report numbers the user chose when Singe was started. They exist so
|
|
a game can be tuned from the launcher rather than by editing the game. All three
|
|
are Hypseus Singe extensions, and Singe does nothing with the values itself: the
|
|
game decides what they mean. The options are described in
|
|
<<commandlineoptions,Command Line Options>>, and they may also be set in a
|
|
settings file (see <<settings,The Settings File>>).
|
|
|
|
None of the three changes during a run, so read each one once and keep it.
|
|
|
|
[#getfvalue]
|
|
==== getFValue
|
|
|
|
[source,text]
|
|
----
|
|
value = getFValue()
|
|
----
|
|
|
|
Hypseus extension. Returns the number given as `--fvalue`, or `0` when the
|
|
option was not used. It is a channel of one number from whoever started Singe to
|
|
the game, meant for a value a front end wants to vary between launches -- a
|
|
difficulty, a starting credit count, a menu index. The engine only carries it.
|
|
|
|
The value runs from `0` to `100000` and keeps three decimals, which is what
|
|
Hypseus keeps, so a game comparing the number sees the same one it saw there.
|
|
|
|
*Returns:* number.
|
|
|
|
*Since:* 3.00 (Hypseus)
|
|
*See also:* <<ratiogetx,ratioGetX>>, <<ratiogety,ratioGetY>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The launcher passes --fvalue 2 for the hard cabinet.
|
|
local difficulty = getFValue()
|
|
if difficulty == 0 then
|
|
difficulty = 1
|
|
end
|
|
----
|
|
|
|
[#ratiogetx]
|
|
==== ratioGetX
|
|
|
|
[source,text]
|
|
----
|
|
factor = ratioGetX()
|
|
----
|
|
|
|
Hypseus extension. Returns the number given as `--xratio`, or `0` when the
|
|
option was not used. Light gun games written for Hypseus read it and stretch
|
|
their own gun coordinates with it, for a cabinet whose screen is not the shape
|
|
the video was mastered in -- `1.33` maps a 16:9 panel onto 4:3 footage. Singe
|
|
does not apply it to anything; the game does the arithmetic, exactly as in
|
|
Hypseus, which is why every game that reads it treats `0` and anything below `1`
|
|
as `1`.
|
|
|
|
The value runs from `0` to `100` and keeps two decimals, as Hypseus keeps it.
|
|
|
|
*Returns:* number.
|
|
|
|
*Since:* 3.00 (Hypseus)
|
|
*See also:* <<ratiogety,ratioGetY>>, <<getfvalue,getFValue>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The shape of the gun coordinates this cabinet wants.
|
|
local ratioX = ratioGetX()
|
|
if ratioX <= 1 then
|
|
ratioX = 1
|
|
end
|
|
local offsetX = (ratioX - 1) * (overlayGetWidth() / 2)
|
|
----
|
|
|
|
[#ratiogety]
|
|
==== ratioGetY
|
|
|
|
[source,text]
|
|
----
|
|
factor = ratioGetY()
|
|
----
|
|
|
|
Hypseus extension. The vertical half of the pair: the number given as
|
|
`--yratio`, or `0` when the option was not used. See <<ratiogetx,ratioGetX>>.
|
|
|
|
*Returns:* number.
|
|
|
|
*Since:* 3.00 (Hypseus)
|
|
*See also:* <<ratiogetx,ratioGetX>>, <<getfvalue,getFValue>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
local ratioY = ratioGetY()
|
|
if ratioY <= 1 then
|
|
ratioY = 1
|
|
end
|
|
----
|
|
|
|
=== Controller
|
|
|
|
Singe supports up to four game controllers, indices `0` through `3`, opened automatically at startup and whenever one is plugged in or removed; there is no `controllerLoad`. Analog axes are reported through `onControllerMoved` (see <<enginecallbacks,Engine Callbacks>>) and polled with `controllerGetAxis`, in SDL's raw range of `-32768` to `32767`. Digital buttons, and each direction of each axis, are switches like keys: they reach `onInputPressed` and `onInputReleased` when `controls.cfg` maps a code from the `GAMEPAD_0` through `GAMEPAD_3` tables (for example `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`), and `controllerGetButton` polls a button's live state. `SINGE_DEAD_ZONE`, from the `DEAD_ZONE` entry in `controls.cfg`, is the threshold the engine itself uses before an axis direction counts as a pressed switch; treat axis motion below it as noise. One axis at a time may be given a different threshold with `DEAD_ZONES`, and the analogue triggers a threshold of their own with `TRIGGER_THRESHOLD` (read back as `SINGE_TRIGGER_THRESHOLD`); see <<deadzones,Dead Zones and Triggers>>. A device SDL does not recognise as a gamepad is opened as one anyway, through a mapping Singe writes for it, so an arcade encoder or a flight stick appears in these slots like any pad -- see <<devices,Every Device Is a Gamepad>>.
|
|
|
|
[#controllerdorumble]
|
|
==== controllerDoRumble
|
|
|
|
[source,text]
|
|
----
|
|
controllerDoRumble(strength, duration)
|
|
controllerDoRumble(controller, strength, duration)
|
|
----
|
|
|
|
Hypseus extension. Rumbles a controller with both motors at one of four strengths for one of four durations. The two argument form addresses controller `0`. A slot with no controller does nothing (Hypseus aborts there, so its games guard with `controllerIsValid`); a strength or duration outside `1` to `4`, or an index outside `0` to `3`, aborts the script. A controller without rumble support ignores the call.
|
|
|
|
*Parameters:*
|
|
|
|
* `controller` -- controller index, `0` through `3`.
|
|
* `strength` -- `1` to `4`, a quarter to full strength.
|
|
* `duration` -- `1` to `4`, giving 150, 300, 600 or 1200 ms.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<controllerisvalid,controllerIsValid>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A short kick on every hit, a long full-strength one when the player dies.
|
|
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)
|
|
----
|
|
|
|
Returns the position of an analog axis as the engine last saw it. The value is updated from every axis event, including motion inside the dead zone, and is `0` for a controller that is not connected or an axis that has never moved. Sticks range from `-32768` to `32767`; triggers rest at `0` and reach `32767` when fully pressed. An index outside `0` to `3`, or an axis outside `0` to `5`, aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `controller` -- controller index, `0` through `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`) or `GAMEPAD_AXIS_RIGHT_TRIGGER` (`5`).
|
|
|
|
*Returns:* integer, `-32768` to `32767`.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<controllergetbutton,controllerGetButton>>, <<oncontrollermoved,onControllerMoved>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Steer with the left stick, throttle with the right 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)
|
|
----
|
|
|
|
Returns whether a button is held right now, read directly from the controller rather than from the event queue. Use it for hold-to-charge and similar cases where `onInputPressed` and `onInputReleased` are awkward. A slot with no controller returns `false`. The button code must come from the `GAMEPAD_N` table of the same controller index; a code from another controller's table, or an axis code, falls outside the button range and aborts the script, as does an index outside `0` to `3`.
|
|
|
|
*Parameters:*
|
|
|
|
* `controller` -- controller index, `0` through `3`.
|
|
* `button` -- a `BUTTON_*` or `DPAD_*` value from the matching table, such as `GAMEPAD_0.BUTTON_A.value`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<controllergetaxis,controllerGetAxis>>, <<keyboardisdown,keyboardIsDown>>, <<oninputpressedoninputreleased,onInputPressed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Charge while A is held, fire on release.
|
|
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)
|
|
----
|
|
|
|
The name of the device in a controller slot, as SDL reports it, or `nil` when the slot is empty. A device SDL did not recognise reports its own name through the mapping Singe wrote for it, so a service screen can list an arcade encoder by name as readily as a branded pad. An index outside `0` to `3` aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `controller` -- controller index, `0` through `3`.
|
|
|
|
*Returns:* string, or `nil`.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<controllerisvalid,controllerIsValid>>, <<mousegetname,mouseGetName>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A service screen listing what is plugged in.
|
|
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
|
|
----
|
|
|
|
[#controllerhowmany]
|
|
==== controllerGetPadding
|
|
|
|
[source,text]
|
|
----
|
|
padding = controllerGetPadding()
|
|
----
|
|
|
|
Hypseus extension, always `0` in Singe. Hypseus adds this offset to a switch code that came from a gamepad, so a game can tell which device pressed what and still recover the switch by subtracting it. Singe delivers gamepad input as ordinary mapped switches carrying no such mark, so it answers `0`: a game testing for the padded range takes its other branch and reads the switch as it stands.
|
|
|
|
*Returns:* an integer, always `0`.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<controllersetpadding,controllerSetPadding>>
|
|
|
|
==== controllerHowMany
|
|
|
|
[source,text]
|
|
----
|
|
count = controllerHowMany()
|
|
----
|
|
|
|
Hypseus extension. How many gamepads are open, of the four slots Singe has. Since every joystick Singe can open becomes a gamepad (see <<everydeviceisagamepad,Every Device Is a Gamepad>>), an arcade encoder or a spinner is counted here as well.
|
|
|
|
The count changes as devices are plugged in and out, so read it when you need it rather than once at startup. It says nothing about which slots are filled: a device may leave slot 0 empty while slot 1 is in use, so <<controllerisvalid,`controllerIsValid`>> is still the way to ask about a particular slot.
|
|
|
|
*Parameters:* none.
|
|
|
|
*Returns:* number, `0` to `4`.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<controllerisvalid,controllerIsValid>>, <<controllergetname,controllerGetName>>, <<mousehowmany,mouseHowMany>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Offer two player only when there is something for player two to hold.
|
|
local twoPlayer = controllerHowMany() >= 2
|
|
----
|
|
|
|
[#controllerisvalid]
|
|
==== controllerIsValid
|
|
|
|
[source,text]
|
|
----
|
|
valid = controllerIsValid(controller)
|
|
----
|
|
|
|
Hypseus extension. Returns whether a controller is open in the slot right now; controllers come and go with hot plugging, so ask before every `controllerDoRumble`. An index outside `0` to `3` aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `controller` -- controller index, `0` through `3`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<controllerdorumble,controllerDoRumble>>, <<controllergetaxis,controllerGetAxis>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Show the pad hints only when there is a pad to hint about.
|
|
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
|
|
----
|
|
|
|
[#debug]
|
|
=== Debug
|
|
|
|
Two calls for seeing what a script is doing: `debugPrint` writes text, and `lineDraw` draws a line over the 3D scene for one frame (`physicsSetDebug` and `navDraw` draw through the same lines). Printed output goes to the console Singe was started from, so it is visible during development and harmless in a cabinet, where `--noconsole` silences it. When Singe runs with `--trace`, every API call including this one is also written with its arguments to `trace.txt` in the data directory, which is the way to get a permanent record. Command line options are listed in the chapter on running Singe; the ZeroBrane Studio integration in `zbstudio/` offers a debugger when printing is not enough.
|
|
|
|
[[traceheader]]
|
|
==== controllerSetPadding
|
|
|
|
[source,text]
|
|
----
|
|
controllerSetPadding(boolean)
|
|
----
|
|
|
|
Hypseus extension, accepted and ignored. See <<controllergetpadding,controllerGetPadding>> for why the offset it asks for does not exist in Singe. Turning it on without marking the events would be worse than leaving it off, because the game would then subtract an offset from switch codes that never carried one.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<controllergetpadding,controllerGetPadding>>
|
|
|
|
==== The Trace Header
|
|
|
|
Whichever of `--program` and `--trace` opened it, `trace.txt` begins with a
|
|
block describing the machine and the run, between two rules so it can be pasted
|
|
whole into a bug report:
|
|
|
|
----
|
|
----------------------------------------------------------------
|
|
Singe: v3.00
|
|
Command: ./Singe -w -p -d data --screen 2 ActionMax
|
|
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: ActionMax/ActionMax.singe
|
|
----------------------------------------------------------------
|
|
----
|
|
|
|
`Renderer` is the renderer that was actually created and the GPU backend it
|
|
chose, or a note that there is no GPU device and 3D is unavailable. `Decoder`
|
|
is what will be tried for video on this machine; which decoder a particular
|
|
video actually got is traced as that video opens, a few lines further down,
|
|
since a codec may offer no hardware path. `Settings` lists what the settings
|
|
file supplied, `none` when there was none. Every game in a queue gets its own
|
|
file and its own header.
|
|
|
|
[#debugprint]
|
|
==== debugPrint
|
|
|
|
[source,text]
|
|
----
|
|
debugPrint(message)
|
|
----
|
|
|
|
Prints a line to the console (standard output). Nothing is printed when Singe was started with `--noconsole`; with `--trace` the call and its message also land in `trace.txt`. The argument must be a string or a number; anything else aborts the script, so wrap values in `tostring` or use `string.format`. It is cheap enough to leave in shipping code but is not throttled, so a print inside `onOverlayUpdate` runs every frame.
|
|
|
|
*Parameters:*
|
|
|
|
* `message` -- the text to print; a number is converted.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<singegetticks,singeGetTicks>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Log why the shot missed, with the frame it happened on.
|
|
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)
|
|
----
|
|
|
|
Draws a straight line between two world-space points over the scene for the current frame only, white unless a color is given. Call it every frame to keep a line on screen, as with the overlay. The line is depth tested against the scene but does not write depth, so it hides behind walls like anything else and never occludes. Nothing is drawn while the scene is disabled. It is meant for seeing what a script is doing (a ray cast, a path, a bounding box); `physicsSetDebug` and `navDraw` draw through the same lines.
|
|
|
|
*Parameters:*
|
|
|
|
* `x0, y0, z0` -- the start point, in world units.
|
|
* `x1, y1, z1` -- the end point, in world units.
|
|
* `r, g, b` -- the line color, `0` to `255` per channel, clamped; white when omitted.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<physicssetdebug,physicsSetDebug>>, <<navdraw,navDraw>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Show where the turret is aiming and whether the shot would land.
|
|
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
|
|
|
|
The *disc* is the main laserdisc video, the one Singe was originally built to emulate. A game has at most one disc, opened at startup from the `VIDEO` named in `games.dat` or from `--disc` / `--framefile` on the command line; a game that names none runs without a disc (see <<withoutadisc,Games Without a Disc>>). All `disc*` functions operate on this single implicit disc and take no handle. Frame numbers are zero based positions in the video file (or in the combined framefile) and are the time reference for every branch decision in a laserdisc game. Without a disc the query functions return `0`, `discGetState` reports `DISC_EJECTED`, and the transport functions trace and do nothing. Extra clips layered on top of the disc are the job of the <<video,Video>> family.
|
|
|
|
[#discaudio]
|
|
==== drawTransparent
|
|
|
|
[source,text]
|
|
----
|
|
drawTransparent(boolean)
|
|
----
|
|
|
|
Hypseus extension, accepted and ignored. It decides whether Hypseus sets colour key 0 on the sprites loaded after it, which is how that engine carries transparency. Singe carries transparency in the alpha channel, as <<spriteresetcolorkey,`spriteResetColorKey`>> already describes, so there is no key to turn on or off.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00
|
|
|
|
==== discAudio
|
|
|
|
[source,text]
|
|
----
|
|
discAudio(channel, enabled)
|
|
----
|
|
|
|
Turns one channel of the disc's soundtrack on or off. The channel is set to the configured laserdisc volume (`--volume_vldp`, default `100`) when `enabled` is `true` and to `0` when it is `false`; the other channel is left as it is. Many original titles carried narration or a data track on one channel only, and this is the call that silences it. A channel other than `1` or `2` terminates the script. Without a disc the call does nothing.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- `1` for the left channel, `2` for the right.
|
|
* `enabled` -- boolean. `true` restores the channel to the configured laserdisc volume, `false` silences it.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<videosetvolume,videoSetVolume>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The right channel of this disc carries the arcade's data track.
|
|
function startGame()
|
|
discAudio(1, true)
|
|
discAudio(2, false)
|
|
discSkipToFrame(SCENE_FIRST_FRAME)
|
|
end
|
|
----
|
|
|
|
[#discaudiosuffix]
|
|
==== discAudioSuffix
|
|
|
|
[source,text]
|
|
----
|
|
opened = discAudioSuffix(suffix)
|
|
----
|
|
|
|
Hypseus extension. Switches the disc to a separate audio file beside its video: `<base><suffix>.ogg`, where the base is the video file's name without its extension, so `movie.m2v` with `"-es"` plays `movie-es.ogg`. The file is opened at the current frame and playback realigns, as `discSetAudioTrack` does, keeping the volume, the play state and the track index when the new file has that many tracks. With a frame file every segment switches. An empty suffix restores the default audio: the `.ogg` beside an `.m2v`, or the audio inside any other container. Returns `true` when the file opened; when it is missing the previous audio keeps playing, nothing is remembered, and `false` comes back (Hypseus leaves silence in that case). The suffix is forgotten when the script reloads. Without a disc the call returns `false`. Use it for a language menu whose tracks ship as separate files; tracks inside one file are the job of `discSetAudioTrack`.
|
|
|
|
*Parameters:*
|
|
|
|
* `suffix` -- string appended to the video's base name, such as `"-es"`; `""` for the default audio.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discaudio,discAudio>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Language files ship as movie.ogg, movie-es.ogg and movie-fr.ogg.
|
|
languages = { { "English", "" }, { "Spanish", "-es" }, { "French", "-fr" } }
|
|
|
|
function applyLanguage(index)
|
|
if not discAudioSuffix(languages[index][2]) then
|
|
discAudioSuffix("")
|
|
index = 1
|
|
end
|
|
settings.language = index
|
|
end
|
|
----
|
|
|
|
[#discchangespeed]
|
|
==== discChangeSpeed
|
|
|
|
[source,text]
|
|
----
|
|
discChangeSpeed(...)
|
|
----
|
|
|
|
Does nothing. Real laserdisc players could run at other than 1x; Singe's software player cannot, so the call accepts any arguments, logs a trace line and returns. It is kept so that pre-2.00 scripts load without edits.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Unimplemented.* Retained for backward compatibility only. There is no replacement; playback speed is fixed at the video file's frame rate.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discplay,discPlay>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Old scripts sometimes bracketed a slow-motion replay with speed changes.
|
|
-- The calls are harmless today, but the replay plays at normal speed.
|
|
discChangeSpeed(0.5)
|
|
discSkipToFrame(REPLAY_FIRST_FRAME)
|
|
discChangeSpeed(1)
|
|
----
|
|
|
|
[#discgetaudiotrack]
|
|
==== discGetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
track = discGetAudioTrack()
|
|
----
|
|
|
|
Returns the zero based index of the audio track the disc is currently playing. At startup the engine selects the track named by `--audio` or the `AUDIO_TRACK` entry in `games.dat` (default `0`) when the disc has that many tracks. Returns `0` without a disc.
|
|
|
|
*Returns:* integer track index.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Cycle to the next dub each time the player presses the language key.
|
|
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()
|
|
----
|
|
|
|
Returns how many audio streams the disc's video file contains. Multiple streams are typically alternate language dubs. A silent video reports `0`, and so does a game without a disc, so guard loops and modulo arithmetic against zero.
|
|
|
|
*Returns:* integer count, `0` or more.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetlanguage,discGetLanguage>>, <<discsetaudiotrack,discSetAudioTrack>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Build a language-select menu from the disc's audio tracks.
|
|
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)
|
|
----
|
|
|
|
The language a subtitle track inside the disc's own container is labelled with, as the ISO 639 code the file carries (`eng`, `fra`, `spa`). A track the file labels with nothing answers an empty string, which is not an error -- plenty of files label nothing.
|
|
|
|
*Parameters:*
|
|
|
|
* `track` -- number; `0` to `discGetSubtitleTracks() - 1`.
|
|
|
|
*Returns:* a string.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>>
|
|
|
|
[#discgetsubtitletracks]
|
|
==== discGetSubtitleTracks
|
|
|
|
[source,text]
|
|
----
|
|
count = discGetSubtitleTracks()
|
|
----
|
|
|
|
How many subtitle tracks the disc's own container holds. `0` with no disc, and `0` for a disc whose subtitles ship as a separate `.srt` -- which is the usual arrangement, and what <<srtload,`srtLoad`>> is for.
|
|
|
|
Tracks that hold pictures rather than words (the DVD and Blu-ray kinds, VobSub and PGS) are counted here so the numbering matches what any other player shows, but <<srtloadtrack,`srtLoadTrack`>> cannot read them: there is nothing to read, only bitmaps of the words.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* a number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<srtloadtrack,srtLoadTrack>>, <<discgetsubtitlelanguage,discGetSubtitleLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Offer whatever languages the disc itself carries.
|
|
for track = 0, discGetSubtitleTracks() - 1 do
|
|
languages[#languages + 1] = { track = track, code = discGetSubtitleLanguage(track) }
|
|
end
|
|
----
|
|
|
|
[#discgetframe]
|
|
==== discGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = discGetFrame()
|
|
----
|
|
|
|
Returns the frame the disc is currently showing. With a framefile the number is the position in the combined sequence, so segment boundaries are invisible to the script. Returns `0` while the disc is stopped and without a disc. Poll it from `onOverlayUpdate` to react to the video reaching a decision point.
|
|
|
|
*Returns:* integer frame number.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsearch,discSearch>>, <<discskiptoframe,discSkipToFrame>>, <<discgetstate,discGetState>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Loop the attract sequence and show a prompt during the duck window.
|
|
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()
|
|
----
|
|
|
|
The number of frames on the disc, which is one past the highest frame
|
|
<<discsearch,discSearch>> will find. A framefile answers for the whole disc
|
|
rather than for the segment playing, so the count does not change as segments
|
|
come and go. Without a disc it answers `0`.
|
|
|
|
*Returns:* integer frame count, `0` without a disc.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<discgetframe,discGetFrame>>, <<discsearch,discSearch>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Seek to the middle of the disc, wherever that is.
|
|
discSearch(math.floor(discGetFrameCount() / 2))
|
|
----
|
|
|
|
[#discgetheight]
|
|
==== discGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = discGetHeight()
|
|
----
|
|
|
|
Returns the height of the disc's video in pixels. Without a disc it returns the canvas height (`CANVAS_Y` in `games.dat` or `--canvas`, default `480`), so it is always the size of the background the overlay is stretched over. Pair it with `discGetWidth` to give the overlay the same resolution as the video.
|
|
|
|
*Returns:* integer height in pixels.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<discgetwidth,discGetWidth>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetheight,vldpGetHeight>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw at native disc resolution so pixel checks and drawing share one grid.
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
crosshairLimitX = discGetWidth() - 1
|
|
crosshairLimitY = discGetHeight() - 1
|
|
----
|
|
|
|
[#discgetlanguage]
|
|
==== discGetLanguage
|
|
|
|
[source,text]
|
|
----
|
|
code = discGetLanguage(track)
|
|
----
|
|
|
|
Returns the language tag stored in the video file for one of the disc's audio tracks. The tag is a three letter ISO 639 code such as `"eng"` or `"jpn"`. Tracks without a three letter tag return `"unk"`, and so does every track when there is no disc. With a disc, a track outside `0` to `discGetAudioTracks() - 1` terminates the script.
|
|
|
|
*Returns:* string language code.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Prefer the Japanese dub when the disc carries one.
|
|
for track = 0, discGetAudioTracks() - 1 do
|
|
if discGetLanguage(track) == "jpn" then
|
|
discSetAudioTrack(track)
|
|
break
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discgetstate]
|
|
==== discGetState
|
|
|
|
[source,text]
|
|
----
|
|
state = discGetState()
|
|
----
|
|
|
|
Returns the transport state of the disc as one of the `DISC_*` constants. A game without a disc reports `DISC_EJECTED`; after `discStop` the disc reports `DISC_STOPPED` until the next `discPlay`, `discSearch` or `discSkipToFrame`; otherwise the state is `DISC_PLAYING` while the video advances and `DISC_PAUSED` when it does not. There is no separate searching state; a disc that is seeking reports `DISC_PAUSED` or `DISC_PLAYING` according to what was requested.
|
|
|
|
*Returns:* one of the following constants, defined by the engine and available to every script.
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Constant | Value | Meaning
|
|
| `DISC_STOPPED` | `2` | Stopped (`discStop` was called)
|
|
| `DISC_PLAYING` | `3` | Playing
|
|
| `DISC_PAUSED` | `4` | Paused
|
|
| `DISC_EJECTED` | `5` | The game has no disc (see <<withoutadisc,Games Without a Disc>>)
|
|
|===
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discstop,discStop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The pause key must not resume a freeze the story script asked for.
|
|
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()
|
|
----
|
|
|
|
Returns the width of the disc's video in pixels. Without a disc it returns the canvas width (`CANVAS_X` in `games.dat` or `--canvas`, default `720`). Together with `discGetHeight` this is the size the overlay is scaled onto, which is why the shipped menu passes both to `overlaySetResolution`.
|
|
|
|
*Returns:* integer width in pixels.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<discgetheight,discGetHeight>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetwidth,vldpGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Center a title sprite on whatever the disc's width happens to be.
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
titleSprite = spriteLoad("graphics/title.png")
|
|
titleX = (discGetWidth() - spriteGetWidth(titleSprite)) / 2
|
|
----
|
|
|
|
[#discpause]
|
|
==== discPause
|
|
|
|
[source,text]
|
|
----
|
|
discPause()
|
|
----
|
|
|
|
Pauses playback on the current frame, which stays on screen. The disc's audio pauses with it. The call is ignored while the disc is stopped and does nothing without a disc. Resume with `discPlay`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discplay,discPlay>>, <<discstop,discStop>>, <<singesetpauseflag,singeSetPauseFlag>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hold the last frame of the scene while the player chooses a door.
|
|
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)
|
|
----
|
|
|
|
Seeks to `frame`, shows it and pauses. This is the same binding as `discSearch` under a second name; the two are interchangeable in every respect, including the wrapping and clamping rules described there. Prefer `discSearch` in new code and keep `discPauseAtFrame` only when porting old scripts.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Legacy alias.* Use <<discsearch,discSearch>> in new code.
|
|
|
|
*Since:* 1.18
|
|
*See also:* <<discsearch,discSearch>>, <<discplay,discPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Ported 1.x code: park on the scene's first frame until the player is ready.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_START1 then
|
|
discPauseAtFrame(SCENE_FIRST_FRAME)
|
|
waitingForPlayer = true
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discplay]
|
|
==== discPlay
|
|
|
|
[source,text]
|
|
----
|
|
discPlay()
|
|
----
|
|
|
|
Starts or resumes playback from the current frame and clears the stopped state set by `discStop`. Audio resumes with the video, and the playback clock restarts from the current frame so a long pause does not cause a jump. Does nothing without a disc, apart from clearing the stopped flag.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discpause,discPause>>, <<discstop,discStop>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Standard attract-mode start: cue the first frame, then roll.
|
|
function attractBegin()
|
|
discSearch(ATTRACT_FIRST_FRAME)
|
|
overlayClear()
|
|
discPlay()
|
|
attractRunning = true
|
|
end
|
|
----
|
|
|
|
[#discsearch]
|
|
==== discSearch
|
|
|
|
[source,text]
|
|
----
|
|
discSearch(frame)
|
|
----
|
|
|
|
Seeks the disc to `frame`, shows that frame and pauses on it, clearing the stopped state if the disc was stopped. The disc holds the frame until `discPlay`, `discSkipToFrame` or another transport call. A negative frame clamps to the first frame and one past the end clamps to the last; with a framefile the segment containing the frame is selected and the position is clamped inside it. Does nothing without a disc.
|
|
|
|
*Parameters:*
|
|
|
|
* `frame` -- target frame number, zero based.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discskiptoframe,discSkipToFrame>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Player picked the left corridor: cue the scene and wait for the go.
|
|
function goLeft()
|
|
discSearch(CORRIDOR_LEFT_FIRST_FRAME)
|
|
soundPlay(SOUND_DOOR)
|
|
waitingForPlayer = true
|
|
end
|
|
----
|
|
|
|
[#discsearchblanking]
|
|
==== discSearchBlanking
|
|
|
|
[source,text]
|
|
----
|
|
discSearchBlanking(...)
|
|
----
|
|
|
|
Does nothing. On laserdisc hardware this chose whether the screen blanked while the player sought a frame; Singe's software player seeks without blanking, so the call accepts any arguments, logs a trace line and returns.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Unimplemented.* Retained for backward compatibility only. There is no replacement.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsearch,discSearch>>, <<discskipblanking,discSkipBlanking>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Pre-2.00 scripts often set the blanking modes once at startup. Harmless.
|
|
discSearchBlanking(false)
|
|
discSkipBlanking(false)
|
|
discSearch(TITLE_FRAME)
|
|
----
|
|
|
|
[#discsetaudiotrack]
|
|
==== discSetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
discSetAudioTrack(track)
|
|
----
|
|
|
|
Switches the disc to another audio track. Queued audio from the old track is dropped and playback realigns, so the change is heard almost immediately. With a disc, a track outside `0` to `discGetAudioTracks() - 1` terminates the script; without a disc the call does nothing. Use it to implement a language menu or to honor a preference saved between sessions.
|
|
|
|
*Parameters:*
|
|
|
|
* `track` -- zero based audio track index.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Apply the language the player chose in the options screen.
|
|
function applyLanguage(choice)
|
|
if choice >= 0 and choice < discGetAudioTracks() then
|
|
discSetAudioTrack(choice)
|
|
settings.language = choice
|
|
saveSettings()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discsetfps]
|
|
==== discSetFPS
|
|
|
|
[source,text]
|
|
----
|
|
discSetFPS(...)
|
|
----
|
|
|
|
Does nothing. The frame rate comes from the video file, so the call accepts any arguments, logs a trace line and returns. Nothing in the shipped `Framework.singe` calls it; only pre-2.00 scripts do.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Unimplemented.* Retained for backward compatibility only. The frame rate is read from the video file and cannot be set from a script.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetframe,discGetFrame>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A ported 1.x script. The call is ignored; frames per second is a property of the file.
|
|
discSetFPS(29.97)
|
|
discSearch(TITLE_FRAME)
|
|
discPlay()
|
|
----
|
|
|
|
[#discskipbackward]
|
|
==== discSkipBackward
|
|
|
|
[source,text]
|
|
----
|
|
discSkipBackward(frames)
|
|
----
|
|
|
|
Seeks backward by `frames` from the current frame without changing the play or pause state: a playing disc keeps playing from the new position, a paused disc stays paused on it. The call is ignored while the disc is stopped and without a disc. The delta is not validated; a negative value skips forward, and a delta larger than the current frame clamps at frame `0`. With a framefile the delta applies to the frame number `discGetFrame` reports, so a skip may cross into an earlier segment.
|
|
|
|
*Parameters:*
|
|
|
|
* `frames` -- number of frames to subtract from the current frame.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discskipforward,discSkipForward>>, <<discstepbackward,discStepBackward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Replay the last second (30 frames) when the player misses a prompt.
|
|
function missedPrompt()
|
|
lives = lives - 1
|
|
discSkipBackward(30)
|
|
promptShown = false
|
|
end
|
|
----
|
|
|
|
[#discskipblanking]
|
|
==== discSkipBlanking
|
|
|
|
[source,text]
|
|
----
|
|
discSkipBlanking(...)
|
|
----
|
|
|
|
Does nothing. It once chose whether the screen blanked during a skip on laserdisc hardware; Singe's software player never blanks, so the call accepts any arguments, logs a trace line and returns.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Unimplemented.* Retained for backward compatibility only. There is no replacement.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsearchblanking,discSearchBlanking>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Pre-2.00 scripts often set the blanking modes once at startup. Harmless.
|
|
discSearchBlanking(false)
|
|
discSkipBlanking(false)
|
|
discSearch(TITLE_FRAME)
|
|
----
|
|
|
|
[#discskipforward]
|
|
==== discSkipForward
|
|
|
|
[source,text]
|
|
----
|
|
discSkipForward(frames)
|
|
----
|
|
|
|
Seeks forward by `frames` from the current frame without changing the play or pause state. Mirror of `discSkipBackward`: ignored while stopped and without a disc, the delta is not validated, and a target past the last frame clamps at the last frame. With a framefile the delta applies to the frame number `discGetFrame` reports, so a skip may cross into a later segment.
|
|
|
|
*Parameters:*
|
|
|
|
* `frames` -- number of frames to add to the current frame.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discskipbackward,discSkipBackward>>, <<discstepforward,discStepForward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Let a returning player skip the two-second scene intro.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 and inSceneIntro then
|
|
discSkipForward(60)
|
|
inSceneIntro = false
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discskiptoframe]
|
|
==== discSkipToFrame
|
|
|
|
[source,text]
|
|
----
|
|
discSkipToFrame(frame)
|
|
----
|
|
|
|
Seeks to `frame` and starts playing from it no matter the disc's state, clearing the stopped state if necessary. Contrast with `discSearch`, which seeks and pauses. A frame before the start or past the end clamps to the first or last frame; with a framefile the segment containing the frame is selected and the position is clamped inside it. Does nothing without a disc, apart from clearing the stopped flag.
|
|
|
|
*Parameters:*
|
|
|
|
* `frame` -- target frame number, zero based.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsearch,discSearch>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Skip the intro on a replay.
|
|
function startGame()
|
|
if hasPlayedBefore then
|
|
discSkipToFrame(GAMEPLAY_FIRST_FRAME)
|
|
else
|
|
discSkipToFrame(INTRO_FIRST_FRAME)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discstepbackward]
|
|
==== discStepBackward
|
|
|
|
[source,text]
|
|
----
|
|
discStepBackward()
|
|
----
|
|
|
|
Moves the disc back exactly one frame and pauses on it, whatever the previous play state. Stepping at frame `0` stays on frame `0`. Ignored while the disc is stopped, like `discSkipBackward`. Intended for frame-accurate debugging and service screens. Does nothing without a disc.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Service screen: left and right nudge the disc one frame at a time.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
discStepBackward()
|
|
elseif what == SWITCH_RIGHT then
|
|
discStepForward()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discstepforward]
|
|
==== discStepForward
|
|
|
|
[source,text]
|
|
----
|
|
discStepForward()
|
|
----
|
|
|
|
Moves the disc forward exactly one frame and pauses on it, whatever the previous play state. Stepping on the last frame stays on the last frame. Like `discStepBackward` it is ignored while the disc is stopped. Does nothing without a disc.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discstepbackward,discStepBackward>>, <<discskipforward,discSkipForward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Frame finder: step through a scene and print the frame number.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(8, 8, "Frame " .. discGetFrame())
|
|
if stepping then
|
|
discStepForward()
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#discstop]
|
|
==== discStop
|
|
|
|
[source,text]
|
|
----
|
|
discStop()
|
|
----
|
|
|
|
Stops the disc. Playback pauses, the video is replaced by the classic blue screen, `discGetState` reports `DISC_STOPPED` and `discGetFrame` returns `0` until the disc is started again. While stopped, `discPause`, `discSkipForward`, `discSkipBackward`, `discStepForward` and `discStepBackward` are ignored. Any of `discPlay`, `discSearch` or `discSkipToFrame` clears the stopped state. A second `discStop` is ignored.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Game over: drop to the blue screen behind the score table.
|
|
function gameOver()
|
|
discStop()
|
|
soundPlay(SOUND_GAME_OVER)
|
|
showScores = true
|
|
end
|
|
----
|
|
|
|
[#emitter]
|
|
=== Emitter
|
|
|
|
Emitters spawn, move and draw particles; see <<particles,Particles>>. Handles
|
|
are integers. A 2D emitter lives in overlay coordinates, Y down, and draws
|
|
when `emitterDraw` asks from `onOverlayUpdate`; a 3D emitter sits on a node,
|
|
lives in world units, and draws itself in the scene. The recipe calls can be
|
|
made in any order, before or after the emitter starts, and take effect on the
|
|
next particles born (a few, noted below, act on the live ones too).
|
|
Everything given as a minimum and maximum is drawn at random between them for
|
|
every particle. Particles advance with the wall clock, at most a tenth of a
|
|
second per frame, and hold while the game is paused.
|
|
|
|
[#emitternew]
|
|
==== emitterNew
|
|
|
|
[source,text]
|
|
----
|
|
emitter = emitterNew()
|
|
emitter = emitterNew(node)
|
|
----
|
|
|
|
Makes an emitter. With no argument it is a 2D one at overlay position `(0, 0)`, moved with `emitterSetPosition`; with a node it is a 3D one whose particles are born at that node's world position, and it needs the 3D scene, so on a machine without one the call raises an error (2D emitters work everywhere). A new emitter is not streaming; call `emitterStart` or `emitterBurst`. Deleting the node deletes its emitters. Defaults: 50 particles a second, living one to two seconds, 30 degree spread, white fading to transparent, normal blending, no gravity, drag, spin, radius, texture, trail or collision, a pool of 1000; a 2D emitter starts them up the screen at 50 to 100 overlay units a second shrinking from 16 to 4, a 3D one up the Y axis at 1 to 2 world units a second shrinking from 0.2 to 0.05.
|
|
|
|
*Returns:* The emitter handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterdelete,emitterDelete>>, <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emitterdraw,emitterDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Exhaust from the ship's engine node; the recipe is set before the stream starts.
|
|
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)
|
|
----
|
|
|
|
Removes the emitter, its live particles and its copy of the texture at once; nothing fades out. Use `emitterStop` instead to let the last particles finish. A 3D emitter is also removed on its own when its node is deleted. A handle that is not a live emitter raises an error, so clear the variable afterward.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitternew,emitterNew>>, <<emitterstop,emitterStop>>, <<emitterisactive,emitterIsActive>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The explosion is over: free its emitter once the last spark has died.
|
|
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)
|
|
----
|
|
|
|
The picture each particle wears: a loaded sprite, or none (one argument, or `nil`) for the built-in soft disc. The emitter keeps its own copy of the sprite's pixels, every frame of an animated sprite included, so the sprite may be unloaded afterward. Setting the texture resets the frame range to frame `0` and moves every live particle to frame `0`, so call `emitterSetFrames` after it, not before. Blending and tint apply to the picture as they do to the disc.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetframes,emitterSetFrames>>, <<spriteload,spriteLoad>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Snow: a sheet of six flake shapes, one picked at random per particle.
|
|
flakes = spriteLoadFrames(DIR .. "Sprites/Flakes", 6)
|
|
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)
|
|
----
|
|
|
|
With an animated sprite as the texture, each new particle takes a random frame from `first` to `last` and keeps it for life; frames count from `0`. Values are clamped to the frames the texture has, and `last` is raised to `first` when it is smaller. Default: frame `0` only, which is also what `emitterSetTexture` resets to. With the built-in disc or a single-frame sprite the range has no effect.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersettexture,emitterSetTexture>>, <<spriteload,spriteLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The puff sheet has four soft puffs, then four harder debris chunks.
|
|
emitterSetTexture(smoke, puffSheet)
|
|
emitterSetFrames(smoke, 0, 3)
|
|
emitterSetTexture(debris, puffSheet)
|
|
emitterSetFrames(debris, 4, 7)
|
|
----
|
|
|
|
[#emittersetblend]
|
|
==== emitterSetBlend
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetBlend(emitter, mode)
|
|
----
|
|
|
|
How particles combine with what is behind them. `PARTICLE_ALPHA` (default) blends normally, for smoke, dust, rain and debris; `PARTICLE_ADD` adds light, for fire, sparks, glows and magic, and never darkens. Takes effect on the live particles at once. Anything other than those two constants raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetcolor,emitterSetColor>>, <<emittersettexture,emitterSetTexture>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Sparks glow; the smoke above them does not.
|
|
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)
|
|
----
|
|
|
|
2D only: where `emitterDraw` puts the particles. `PARTICLE_OVER` (default) draws above everything on the overlay; `PARTICLE_UNDER` draws beneath the overlay and above the video and the 3D scene, for smoke behind the score. On a 3D emitter the setting is accepted and ignored. Anything other than those two constants raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterdraw,emitterDraw>>, <<emittersetposition,emitterSetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Rain falls behind the HUD, the hit sparks in front of it.
|
|
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)
|
|
----
|
|
|
|
How many particles a second a started emitter makes; default `50`, negative values become `0`. Fractions accumulate, so `0.5` makes one every two seconds. Change it while streaming to throttle a flame or a thruster. A full pool (`emitterSetMax`) makes no more until some die, whatever the rate.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emittersetmax,emitterSetMax>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The thruster's plume follows the throttle.
|
|
function onControllerMoved(controller, axis, value)
|
|
if axis == AXIS_TRIGGER_RIGHT then
|
|
throttle = value
|
|
emitterSetRate(plume, 30 + 300 * throttle)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#emittersetlife]
|
|
==== emitterSetLife
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetLife(emitter, minSeconds, maxSeconds)
|
|
----
|
|
|
|
How long each new particle lives, drawn at random between the two; default `1` to `2`. Its size and color run from their start to their end over that time, so life sets the pace of every fade. The minimum is held at `0.001` or above and the maximum is raised to the minimum when it is smaller. Live particles keep the life they were born with.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetsize,emitterSetSize>>, <<emittersetcolor,emitterSetColor>>, <<emittersetrate,emitterSetRate>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Muzzle flash: a burst that is gone in a tenth of a second.
|
|
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)
|
|
----
|
|
|
|
3D only: fades a particle out over `distance` world units where it meets geometry behind it, so a smoke puff does not draw a hard line where it crosses a wall and a fountain sinks into its pool instead of cutting it. `0` (the default) switches it off; negative values become `0`. While any emitter uses it the scene's depth is rendered once more per frame, so a Raspberry Pi should use it sparingly. A 2D emitter stores the value and ignores it.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetlit,emitterSetLit>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Smoke from the brazier softens against the post it rises past.
|
|
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)
|
|
----
|
|
|
|
Starting speed of each new particle, drawn at random between the two, in overlay units a second (2D) or world units a second (3D); defaults `50` to `100` in 2D and `1` to `2` in 3D. The maximum is raised to the minimum when it is smaller. Speed sets how far a particle gets before drag, gravity and its life take over.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetdirection,emitterSetDirection>>, <<emittersetdrag,emitterSetDrag>>, <<emittersetgravity,emitterSetGravity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A fountain throws drops hard and straight; dust barely moves.
|
|
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)
|
|
----
|
|
|
|
The mean direction new particles set off in; only the direction matters, not the length. 2D default `(0, -1)`, up the screen since overlay Y grows downward; 3D default `(0, 1, 0)`, up the world Y axis. A 2D emitter ignores `z`; a 3D emitter given two values gets `z = 0`. A zero vector is treated as `(0, -1, 0)`. The direction is in world axes for a 3D emitter, not the node's, so turn it yourself when the node turns.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetspread,emitterSetSpread>>, <<emittersetspeed,emitterSetSpeed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Exhaust streams out behind the ship whichever way it faces.
|
|
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)
|
|
----
|
|
|
|
Half the angle of the cone (3D) or fan (2D) around the direction that new particles are thrown into, `0` to `180`; `0` is a straight line, `180` is every direction. Default `30`; values outside the range are clamped. In 3D the directions are spread evenly over the cone's cap, so a wide cone does not bunch toward its axis.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetdirection,emitterSetDirection>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fireworks go everywhere; the torch flame stays in a tight column.
|
|
emitterSetSpread(fireworks, 180)
|
|
emitterSetSpread(flame, 15)
|
|
----
|
|
|
|
[#emittersetgravity]
|
|
==== emitterSetGravity
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetGravity(emitter, x, y)
|
|
emitterSetGravity(emitter, x, y, z)
|
|
----
|
|
|
|
A constant acceleration on every particle, in overlay units (2D) or world units (3D) a second squared: `(0, 500)` pulls 2D sparks down the screen, `(0, -20)` lets 2D smoke rise, `(0, -9.8, 0)` is Earth in the scene. Default none. It applies to the live particles from the next frame. Particles are not physics bodies; give them `emitterSetCollide` to bounce off a floor or the scene.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetdrag,emitterSetDrag>>, <<emittersetcollide,emitterSetCollide>>, <<emittersetspeed,emitterSetSpeed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Sparks fall, smoke rises, both from the same grinder.
|
|
emitterSetGravity(sparks, 0, 500)
|
|
emitterSetGravity(smoke, 0, -20)
|
|
|
|
-- Underwater, the bubbles head for the surface.
|
|
emitterSetGravity(bubbles, 0, 3, 0)
|
|
----
|
|
|
|
[#emittersetdrag]
|
|
==== emitterSetDrag
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetDrag(emitter, perSecond)
|
|
----
|
|
|
|
How quickly particles lose their velocity: every frame the velocity is multiplied by `1 - perSecond * dt`, never below `0`, so `1` takes most of the speed away within a second and `0` (the default) keeps it forever. Negative values become `0`. Smoke, embers and fireworks slow down; sparks and rain do not. Applies to the live particles from the next frame.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetgravity,emitterSetGravity>>, <<emittersetspeed,emitterSetSpeed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A firework shell: fast out, then hanging and falling.
|
|
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)
|
|
----
|
|
|
|
Size at birth and at death, in overlay units (2D) or world units (3D), changing evenly over the particle's life; defaults `16` to `4` in 2D and `0.2` to `0.05` in 3D. `variation` (`0` to `1`, default `0`) scales both sizes of each new particle by one random factor between `1 - variation` and `1 + variation`, so a puff is a little bigger or smaller for its whole life. Negative sizes become `0` and `variation` is clamped. A textured particle is drawn as a square of that size; a wider picture is squashed.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetlife,emitterSetLife>>, <<emittersetcolor,emitterSetColor>>, <<emittersetspin,emitterSetSpin>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Smoke grows as it thins; sparks shrink to nothing.
|
|
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)
|
|
----
|
|
|
|
What particles bounce off. `COLLIDE_NONE` (default) is nothing; `COLLIDE_FLOOR` is a horizontal plane at height `floor`, below which (larger Y in 2D, smaller Y in 3D) a particle is put back on the plane and reflected; `COLLIDE_SCENE` is the physics world, for 3D emitters only, by a ray cast per particle along its motion each frame, capped at `200` casts per emitter per frame and resumed where they left off, so a big emitter spreads the cost over a few frames. On a 2D emitter `COLLIDE_SCENE` is accepted and does nothing. A mode other than those three raises an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `mode` -- `COLLIDE_NONE`, `COLLIDE_FLOOR` or `COLLIDE_SCENE`.
|
|
* `bounce` -- how much speed survives a hit, `0` to `1`, default `0.5`; clamped.
|
|
* `friction` -- how much sliding speed is lost per hit, `0` to `1`, default `0.2`; clamped.
|
|
* `floor` -- height of the floor plane, in world units for a 3D emitter or overlay units for a 2D one, default `0`. Used by `COLLIDE_FLOOR` only.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetgravity,emitterSetGravity>>, <<emittersettrail,emitterSetTrail>>, <<emittersetsoftness,emitterSetSoftness>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Sparks bounce on the workshop floor (overlay row 440) and leave streaks.
|
|
sparks = emitterNew()
|
|
emitterSetBlend(sparks, PARTICLE_ADD)
|
|
emitterSetGravity(sparks, 0, 500)
|
|
emitterSetCollide(sparks, COLLIDE_FLOOR, 0.4, 0.2, 440)
|
|
emitterSetTrail(sparks, 6, 3)
|
|
|
|
-- Embers in the scene bounce off whatever the physics world holds.
|
|
emitterSetCollide(embers, COLLIDE_SCENE, 0.3, 0.5)
|
|
----
|
|
|
|
[#emittersetcolor]
|
|
==== emitterSetColor
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetColor(emitter, r, g, b, a, r2, g2, b2, a2)
|
|
----
|
|
|
|
Color and opacity at birth (`r, g, b, a`) and at death (`r2, g2, b2, a2`), each `0` to `255`, changing evenly over the particle's life; values outside the range are clamped. Default white and opaque to white and transparent. The tint multiplies the texture or the built-in disc, so a white picture takes the color exactly. A fade in and out needs two emitters or a texture with the shape in it; one recipe runs one straight ramp.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetblend,emitterSetBlend>>, <<emittersetlife,emitterSetLife>>, <<emittersetsize,emitterSetSize>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Flame: bright yellow at the base, dying to dim red and gone.
|
|
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
|
|
-- Dust motes fade in from nothing and hang; their life ends them.
|
|
emitterSetColor(dust, 255, 240, 200, 0, 255, 240, 200, 160)
|
|
----
|
|
|
|
[#emittersetspin]
|
|
==== emitterSetSpin
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetSpin(emitter, min, max)
|
|
----
|
|
|
|
Rotation speed of each new particle in degrees a second, drawn at random between the two; negative values spin the other way, and a range that straddles `0` gives both. Every particle starts at a random angle whether or not it spins. Default none. The maximum is raised to the minimum when it is smaller. Spin shows on a textured particle or a trail; the built-in disc is round and looks the same at any angle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersettexture,emitterSetTexture>>, <<emittersetsize,emitterSetSize>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Leaves tumble; embers hardly turn.
|
|
emitterSetTexture(leaves, leafSprite)
|
|
emitterSetSpin(leaves, -180, 180)
|
|
emitterSetSpin(embers, -20, 20)
|
|
----
|
|
|
|
[#emittersetradius]
|
|
==== emitterSetRadius
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetRadius(emitter, radius)
|
|
----
|
|
|
|
New particles are born anywhere within this distance of the emitter's origin, evenly over a disc (2D) or a ball (3D), in overlay or world units. Default `0`, a point; negative values become `0`. A radius fills a volume, a torch head or a patch of dust in the air, where a point would give a needle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetspread,emitterSetSpread>>, <<emittersetposition,emitterSetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Dust in a room-sized ball of air, most of it already there when the level starts.
|
|
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)
|
|
----
|
|
|
|
3D only: shades each particle by the scene's lights and sky as if it were a small sphere facing the camera, so smoke takes a brazier's color and dust glints in the sun. Off by default; shadows are not consulted. With `PARTICLE_ADD` the lighting is still applied to the tint before it is added. A 2D emitter stores the value and ignores it.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetsoftness,emitterSetSoftness>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Smoke over each brazier picks up its firelight.
|
|
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)
|
|
----
|
|
|
|
Whether live particles follow the emitter. `false` (default) leaves each one where it was born, so a torch carried through a room leaves smoke behind it; `true` keeps the whole cloud relative to the emitter's origin, so a thruster plume moves with the ship. Switching it moves the live particles between frames of reference without a jump. On a 3D emitter the origin is the node's world position; on a 2D one it is the `emitterSetPosition` point, so a local 2D cloud slides with that too. Trails follow the same rule.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetposition,emitterSetPosition>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A magic aura that stays wrapped around the wizard as she walks.
|
|
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)
|
|
----
|
|
|
|
The pool: how many particles may be alive at once, default `1000`. A full pool makes no more, from the stream or a burst, until some die. Changing the count rebuilds the pool and kills every live particle, so set it before starting. Size the pool to rate times longest life, plus the biggest burst. A count below `1` raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>, <<emittergetcount,emitterGetCount>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- 300 drops a second living up to 1.6 seconds needs about 480; leave room.
|
|
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)
|
|
----
|
|
|
|
2D only: where new particles are born, in overlay coordinates; default `(0, 0)`. Set it every frame to follow a sprite or a 2D physics body. Live particles stay where they are unless `emitterSetLocal` is on. A 3D emitter follows its node and raises an error here; move the node instead.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterdraw,emitterDraw>>, <<emittersetlocal,emitterSetLocal>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Sparks fly from wherever the grinder sprite is this frame.
|
|
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)
|
|
----
|
|
|
|
A ribbon behind every particle through its last `length` positions, one recorded per frame (`2` to `16`; `0` or `1` for none, the default), `width` across in world or overlay units, in 2D and 3D alike. The ribbon carries the particle's current tint, fades to transparent toward its tail, and is textured by the middle column of the particle's picture so the disc's soft edge becomes the ribbon's. Values are clamped to the range. Changing the length rebuilds the trail storage, so live particles start their ribbons again. Sparks, embers, tracer rounds, comets.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetcollide,emitterSetCollide>>, <<emittersetsize,emitterSetSize>>, <<emittersetlocal,emitterSetLocal>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Tracer rounds: tiny, fast, and all streak.
|
|
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)
|
|
----
|
|
|
|
Begins the steady stream at the rate set; from then on the emitter makes particles every frame until `emitterStop`. Starting an emitter that is already streaming changes nothing. The stream needs no drawing call to run; a 2D emitter that is streaming but not drawn still spends its pool.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterstop,emitterStop>>, <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The engine lights when the player throttles up and dies when they cut it.
|
|
function onThrottle(open)
|
|
if open then
|
|
emitterStart(exhaust)
|
|
else
|
|
emitterStop(exhaust)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#emitterstop]
|
|
==== emitterStop
|
|
|
|
[source,text]
|
|
----
|
|
emitterStop(emitter)
|
|
----
|
|
|
|
Ends the stream; the live particles finish their lives and any fraction of a particle owed is dropped. `emitterBurst` still works on a stopped emitter, and `emitterIsActive` stays `true` until the last particle dies. Use `emitterClear` to remove the particles at once as well.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterclear,emitterClear>>, <<emitterstart,emitterStart>>, <<emitterisactive,emitterIsActive>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The torch goes out: stop the flame, let the last smoke drift away.
|
|
function onTorchDoused()
|
|
emitterStop(flame)
|
|
emitterStop(smoke)
|
|
soundPlay(hiss)
|
|
end
|
|
----
|
|
|
|
[#emitterburst]
|
|
==== emitterBurst
|
|
|
|
[source,text]
|
|
----
|
|
emitterBurst(emitter, count)
|
|
----
|
|
|
|
Makes `count` particles at once from the current recipe, stream or no stream: an explosion, a splash, a puff. Only as many as the pool has room for are made; `emitterSetMax` sets the room. A 2D emitter bursts at its `emitterSetPosition` point, a 3D one at its node, spread over `emitterSetRadius`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterstart,emitterStart>>, <<emittersetmax,emitterSetMax>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fireworks: a shell bursts somewhere over the disc every second or so.
|
|
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)
|
|
----
|
|
|
|
Kills every live particle now and forgets any fraction of a particle owed. The stream is not stopped: a started emitter goes on making new particles next frame, so call `emitterStop` as well to empty it for good. Use it when a scene changes and the old sparks must not linger.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterstop,emitterStop>>, <<emitterdelete,emitterDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Restarting the level: the emitters stay, the old particles go.
|
|
function resetLevel()
|
|
for _, emitter in ipairs(levelEmitters) do
|
|
emitterStop(emitter)
|
|
emitterClear(emitter)
|
|
end
|
|
placePlayer()
|
|
end
|
|
----
|
|
|
|
[#emitterdraw]
|
|
==== emitterDraw
|
|
|
|
[source,text]
|
|
----
|
|
emitterDraw(emitter)
|
|
----
|
|
|
|
2D only, from `onOverlayUpdate`: queues the emitter's particles to be drawn this frame, above or beneath the overlay per `emitterSetLayer`, in overlay coordinates mapped onto the window. Emitters on the same layer draw in the order they were queued. The queue empties every frame, so the call belongs in every frame the particles should show; not drawing an emitter does not stop it. Up to `64` emitters per layer draw in a frame. A 3D emitter draws itself in the scene and raises an error here.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetlayer,emitterSetLayer>>, <<emittersetposition,emitterSetPosition>>, <<onoverlayupdate,onOverlayUpdate>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
How many of the emitter's particles are alive this frame, `0` to the pool size. Useful for tuning a pool, for an on-screen budget, or for knowing when a burst has died away.
|
|
|
|
*Returns:* The live count, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emittersetmax,emitterSetMax>>, <<emitterisactive,emitterIsActive>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Development overlay: how much of each pool is in use.
|
|
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)
|
|
----
|
|
|
|
Whether the emitter is streaming or still has live particles: `true` from `emitterStart` or `emitterBurst` until the stream is stopped and the last particle has died, `false` once the explosion is over. A streaming emitter whose pool happens to be empty is still active.
|
|
|
|
*Returns:* Boolean.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<emitterstop,emitterStop>>, <<emittergetcount,emitterGetCount>>, <<emitterdelete,emitterDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- One-shot explosions live in a list and are freed when they burn out.
|
|
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]
|
|
=== Font
|
|
|
|
Singe has two text renderers. The built-in console font is drawn by <<overlayprint,overlayPrint>> and needs no assets; the `font*` family loads TrueType files at a fixed point size and renders them either straight onto the overlay with `fontPrint` or into a reusable sprite with `fontToSprite`. Font handles are integers from `fontLoad`, and the engine keeps one globally selected font, which `fontLoad` sets to the font it just loaded and `fontSelect` changes. Text is rendered in the current foreground color at the quality chosen by `fontQuality`, and text positions are overlay coordinates. Release fonts with `fontUnload`; `Singe/FreeSansBold.ttf` ships with the engine and is always available.
|
|
|
|
[#fontload]
|
|
==== fontLoad
|
|
|
|
[source,text]
|
|
----
|
|
result = fontLoad(filename, pointSize)
|
|
----
|
|
|
|
Opens a TrueType font at one point size and makes it the selected font. The size is fixed for the life of the handle, so load the same file once per size you need. The name goes through the virtual file system like any other asset; prepend `DIR` for files shipped with your game, or use `Singe/FreeSansBold.ttf` for the bundled face. A file that cannot be opened ends the script with `Unable to open` and the name.
|
|
|
|
*Parameters:*
|
|
|
|
* `filename` -- path to a `.ttf` file.
|
|
* `pointSize` -- integer point size.
|
|
|
|
*Returns:* integer font handle.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontselect,fontSelect>>, <<fontunload,fontUnload>>, <<fontquality,fontQuality>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Two sizes of the bundled face: large for the title, small for the HUD.
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
titleFont = fontLoad("Singe/FreeSansBold.ttf", 32)
|
|
hudFont = fontLoad("Singe/FreeSansBold.ttf", 18)
|
|
|
|
fontSelect(titleFont)
|
|
titleSprite = fontToSprite("SPACE ACE")
|
|
fontSelect(hudFont)
|
|
----
|
|
|
|
[#fontprint]
|
|
==== fontPrint
|
|
|
|
[source,text]
|
|
----
|
|
fontPrint(x, y, text)
|
|
----
|
|
|
|
Renders a string with the selected font, the current quality, and the current foreground color (plus the background color for `FONT_QUALITY_SHADED`), then blits it onto the overlay with its top-left corner at `(x, y)`. Call it from `onOverlayUpdate`. A new text image is rendered and discarded on every call, which is fine for a few lines but expensive for text that never changes; `fontToSprite` renders once and draws many times. With no font selected the script ends with an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- top-left corner in overlay coordinates; fractions are truncated.
|
|
* `text` -- the string to render.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fonttosprite,fontToSprite>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>, <<overlayprint,overlayPrint>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Live audio calibration readout; the text changes every frame, so render it directly.
|
|
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)
|
|
----
|
|
|
|
Selects how `fontPrint` and `fontToSprite` rasterize text from now on. The setting is global, not per font, and starts at `FONT_QUALITY_SOLID`. Sprites already made by `fontToSprite` keep the quality they were rendered with. A value outside the three constants ends the script with an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `mode` -- `FONT_QUALITY_SOLID` (`1`): fastest, one-bit edges, no anti-aliasing. `FONT_QUALITY_SHADED` (`2`): anti-aliased glyphs on a solid rectangle of the background color. `FONT_QUALITY_BLENDED` (`3`): anti-aliased glyphs with per-pixel alpha that blend over anything; the usual choice.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>, <<colorbackground,colorBackground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Blended text over the video, but a shaded label box for the debug readout.
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
titleSprite = fontToSprite("DRAGON'S LAIR")
|
|
|
|
colorBackground(0, 0, 0, 255)
|
|
fontQuality(FONT_QUALITY_SHADED)
|
|
debugLabel = fontToSprite("FRAME")
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
----
|
|
|
|
[#fontselect]
|
|
==== fontSelect
|
|
|
|
[source,text]
|
|
----
|
|
fontSelect(id)
|
|
----
|
|
|
|
Makes a loaded font the one used by `fontPrint` and `fontToSprite`. The selection persists until the next `fontSelect` or `fontLoad`, since loading also selects. It can be changed as often as needed, including several times within one `onOverlayUpdate`. An unknown or unloaded handle ends the script with an error.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Title in the large face, score in the small one, every frame.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
fontSelect(titleFont)
|
|
fontPrint(TITLE_X, 40, "DRAGON'S LAIR")
|
|
fontSelect(hudFont)
|
|
fontPrint(8, 8, "Score: " .. score)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#fonttosprite]
|
|
==== fontToSprite
|
|
|
|
[source,text]
|
|
----
|
|
result = fontToSprite(text)
|
|
----
|
|
|
|
Renders a string with the selected font, quality, and colors into a new sprite and returns its handle. The sprite is independent of the font from then on, so it survives `fontUnload`, and it works with every `sprite*` call: draw it, measure it, scale or rotate it, and free it with `spriteUnload`. Render once and draw many times for labels and headings; for a string that changes, render a new sprite and unload the old one. With no font selected the script ends with an error.
|
|
|
|
*Returns:* integer sprite handle.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontprint,fontPrint>>, <<spritedraw,spriteDraw>>, <<spritegetwidth,spriteGetWidth>>, <<spriteunload,spriteUnload>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Pre-render the game description, wrapping lines to the panel width.
|
|
descriptionLines = {}
|
|
for _, line in ipairs(wrapText(GAME_LIST[selected].DESCRIPTION, TEXT_W)) do
|
|
table.insert(descriptionLines, fontToSprite(line))
|
|
end
|
|
|
|
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
|
|
----
|
|
|
|
[#fontunload]
|
|
==== fontUnload
|
|
|
|
[source,text]
|
|
----
|
|
fontUnload(id)
|
|
----
|
|
|
|
Closes a font and invalidates its handle. Sprites made from it with `fontToSprite` are unaffected. If the unloaded font was the selected one, no font is selected afterward and the next `fontPrint` or `fontToSprite` ends the script with an error until `fontSelect` or `fontLoad` picks another.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<fontload,fontLoad>>, <<fontselect,fontSelect>>, <<onshutdown,onShutdown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Release the fonts and the text sprites they produced.
|
|
function onShutdown()
|
|
spriteUnload(titleSprite)
|
|
for _, line in ipairs(descriptionLines) do
|
|
spriteUnload(line)
|
|
end
|
|
fontUnload(titleFont)
|
|
fontUnload(hudFont)
|
|
end
|
|
----
|
|
|
|
[#guiref]
|
|
=== Gui
|
|
|
|
A GUI is an RmlUi document tree of a fixed size rendered into its own texture every frame: `guiNew` makes one and returns an integer handle, `guiLoad` puts an RML document in it and returns a document handle, `guiDraw` composites it over the overlay and `materialSetGui` shows it on a material in the scene. Documents are HTML-like RML styled by CSS-like RCSS, loaded through the virtual file system like sprites; `Singe/gui.rcss` is the shipped theme and `Singe/FreeSansBold.ttf` the fallback face. The by-id helpers `guiGetValue`, `guiSetValue` and `guiSetHandler` reach elements without the object model; everything else is RmlUi's Lua API on the `rmlui` global, where a GUI is the context named `gui` followed by its handle. Up to sixteen GUIs exist at once, each with thirty-two documents. A GUI needs the GPU device, so `guiNew` raises an error on a machine without one; a bad GUI handle raises `No GUI N`, and a bad document handle `No document N in GUI G`. See <<gui,GUI>> in Game Development.
|
|
|
|
[#guiclose]
|
|
==== guiClose
|
|
|
|
[source,text]
|
|
----
|
|
guiClose(gui, document)
|
|
----
|
|
|
|
Unloads the document from the GUI, dropping its elements, its handlers and its scripts. The document handle is invalid afterwards. A document closed by its own script through RmlUi's API (`document:Close()`) is unloaded the same way. Raises `No document N in GUI G` for a handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guiload,guiLoad>>, <<guihide,guiHide>>, <<guidelete,guiDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The high score entry page is only needed once; drop it after the name is in.
|
|
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)
|
|
----
|
|
|
|
Frees the GUI, every document in it and its texture, and stops the per-frame update and render it cost. Materials showing it through `materialSetGui` go back to their own texture. The handle is invalid afterwards and raises an error if used again; since a GUI keeps rendering whether or not it is drawn, delete the ones a game is finished with rather than hiding them.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guinew,guiNew>>, <<guiclose,guiClose>>, <<materialsetgui,materialSetGui>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The options page lives only while the player is in it.
|
|
function closeOptions()
|
|
guiDelete(optionsGui)
|
|
optionsGui = nil
|
|
discPlay()
|
|
end
|
|
----
|
|
|
|
[#guidraw]
|
|
==== guiDraw
|
|
|
|
[source,text]
|
|
----
|
|
guiDraw(gui)
|
|
guiDraw(gui, x, y)
|
|
guiDraw(gui, x, y, width, height)
|
|
----
|
|
|
|
Composites the GUI's texture over the overlay for this frame, in overlay coordinates, so it obeys the Sinden border and the overscan scale like everything else on the overlay: covering the whole overlay with one argument, at its own size with a corner at `x, y`, or scaled into the `width` by `height` rectangle. The call lasts one frame; make it from `onOverlayUpdate` (or `singeMain`) every frame the GUI should be visible. GUIs draw above the overlay in the order called (the last on top), under `PARTICLE_OVER` particles and the pause indicator, and the GUI drawn last under the pointer is the one that receives the mouse. Up to sixteen calls are honored per frame; more are ignored. Any other argument count raises `Expected 1, 3 or 5 arguments`.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- the top left corner, in overlay coordinates.
|
|
* `width`, `height` -- the size to draw at, in overlay pixels; the GUI's own size when left out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guinew,guiNew>>, <<materialsetgui,materialSetGui>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The HUD sits in the bottom right corner at a third of the overlay's width.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
guiSetValue(hud, hudPage, "lives", tostring(lives))
|
|
guiDraw(hud, overlayGetWidth() * 0.66, overlayGetHeight() * 0.8, overlayGetWidth() * 0.33, overlayGetHeight() * 0.2)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#guidrawscreen]
|
|
==== guiDrawScreen
|
|
|
|
[source,text]
|
|
----
|
|
guiDrawScreen(gui)
|
|
guiDrawScreen(gui, x, y)
|
|
guiDrawScreen(gui, x, y, width, height)
|
|
----
|
|
|
|
The same as <<guidraw,`guiDraw`>>, except that the rectangle is measured across the whole window instead of across the picture. The coordinates are still overlay coordinates -- `0, 0` is the top left of the window and `overlayGetWidth(), overlayGetHeight()` the bottom right -- so a GUI can sit on the bezel artwork beside the picture, above it, or below it. With no bezel, and with no `--scalefactor`, `--shiftx` or `--shifty`, the picture is the whole window and this is exactly `guiDraw`.
|
|
|
|
Screen space GUIs draw after everything else, the bezel artwork included, and share the sixteen draws a frame with `guiDraw`. A click finds them the same way: the pointer is carried into screen space before it is tested, so a screen space GUI that takes input receives the mouse wherever it was drawn.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- the top left corner, in overlay coordinates measured across the window.
|
|
* `width`, `height` -- the size to draw at; the GUI's own size when left out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guidraw,guiDraw>>, <<bezels,Bezels>>, <<scorebezelenable,scoreBezelEnable>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A map on the left hand pillar of the cabinet artwork, outside the picture.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
guiDrawScreen(mapGui, 8, 44, 38, 114)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#guigetheight]
|
|
==== getUserString
|
|
|
|
[source,text]
|
|
----
|
|
id = getUserString()
|
|
----
|
|
|
|
Hypseus extension, always an empty string. Despite the name it is not a string the user set: Hypseus answers the machine install ID it sends with network statistics, taken from the Windows `MachineGuid` or the Linux machine ID. Singe sends nothing anywhere and will not hand a game a fingerprint of the machine it is running on.
|
|
|
|
*Returns:* a string, always empty.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<allowsocketcall,allowSocketCall>>
|
|
|
|
==== guiGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = guiGetHeight(gui)
|
|
----
|
|
|
|
The GUI's height in its own pixels, as given to `guiNew`. Raises an error for a handle that is not valid.
|
|
|
|
*Returns:* An integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guigetwidth,guiGetWidth>>, <<guinew,guiNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Center a small dialog on the overlay.
|
|
local x = (overlayGetWidth() - guiGetWidth(dialog)) / 2
|
|
local y = (overlayGetHeight() - guiGetHeight(dialog)) / 2
|
|
guiDraw(dialog, x, y)
|
|
----
|
|
|
|
[#guigetstats]
|
|
==== guiGetStats
|
|
|
|
[source,text]
|
|
----
|
|
stats = guiGetStats()
|
|
----
|
|
|
|
What rendering every GUI cost in the last completed frame, for tuning documents that use filters, masks and shadows (each of which draws through a layer the size of the GUI). The counters cover all GUIs together.
|
|
|
|
*Returns:* A table with the fields `drawCalls` (indexed draws, including mask writes, composites and filter passes), `layerPushes` (layers entered for filters, mask images and box shadows), `filterPasses` (full-size passes and blits inside filter chains; a blur costs several), `maskWrites` (draws into the stencil clip mask), `layerBytes` (bytes of layer textures currently allocated; each is the size of its GUI at four bytes a pixel) and `passes` (render passes begun; every layer switch begins one).
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guinew,guiNew>>, <<gui,GUI>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Watch what a screen full of effects costs.
|
|
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)
|
|
----
|
|
|
|
The value of the element with that `id` in the document, as a string: a form control's value (a text field's text, a `select` option's `value`, a range in RmlUi's own formatting such as `"85.000000"`, a checkbox's or radio button's `value` attribute, `"on"` when it has none, whether or not it is checked; its state is its `checked` attribute, read through RmlUi's API), or, for any other element, its inner RML. Returns `nil` when no element has that id, so a page can be probed without an error; a bad GUI or document handle raises one. Values longer than 4095 characters are cut.
|
|
|
|
*Returns:* A string, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guisetvalue,guiSetValue>>, <<guisethandler,guiSetHandler>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Read the whole form when Start is pressed.
|
|
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)
|
|
----
|
|
|
|
The GUI's width in its own pixels, as given to `guiNew`. Raises an error for a handle that is not valid.
|
|
|
|
*Returns:* An integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guigetheight,guiGetHeight>>, <<guinew,guiNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Is the probed point in the right half of the monitor's page?
|
|
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)
|
|
----
|
|
|
|
Hides the document: it is not drawn and takes no input, but it keeps its elements and values and `guiShow` brings it back as it was. Raises `No document N in GUI G` for a handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guishow,guiShow>>, <<guiclose,guiClose>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Two pages in one GUI: the options page hides while the controls page shows.
|
|
guiSetHandler(gui, options, "controls", "click", function()
|
|
guiHide(gui, options)
|
|
guiShow(gui, controls)
|
|
end)
|
|
----
|
|
|
|
[#guiload]
|
|
==== guiLoad
|
|
|
|
[source,text]
|
|
----
|
|
document = guiLoad(gui, filename)
|
|
----
|
|
|
|
Loads an RML document into the GUI, shows it, and returns an integer document handle for the other calls. The name goes through the virtual file system like a sprite's, resolved relative to the directory Singe was started in, so prepend `DIR` for files shipped with your game; style sheets, images and fonts named inside the document resolve relative to the file that names them and then through the same lookup, so a packed game finds them. The document's `<script>` blocks and `on*` attributes run on the game's Lua state, and it is reachable as `rmlui.contexts["guiN"].documents["<body id>"]`. A document that cannot be loaded ends the script with the name and RmlUi's reason (a missing file, a parse error); RmlUi's own warnings about the markup and style go to the console. Each GUI holds up to thirty-two documents.
|
|
|
|
*Returns:* The document handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guinew,guiNew>>, <<guishow,guiShow>>, <<guihide,guiHide>>, <<guiclose,guiClose>>, <<guiloadfont,guiLoadFont>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The title page and the HUD share one GUI the size of the overlay.
|
|
gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
|
title = guiLoad(gui, DIR .. "gui/title.rml")
|
|
hud = guiLoad(gui, DIR .. "gui/hud.rml")
|
|
guiHide(gui, hud)
|
|
----
|
|
|
|
[#guiloadfont]
|
|
==== guiLoadFont
|
|
|
|
[source,text]
|
|
----
|
|
guiLoadFont(filename)
|
|
guiLoadFont(filename, fallback)
|
|
----
|
|
|
|
Loads a TrueType or OpenType face for every GUI, so documents can name it by the family name inside the file in `font-family` (with `font-weight` and `font-style` picking among the faces of one family). With `fallback` `true` the face also serves any glyph no other face has, which is what the engine's own `Singe/FreeSansBold.ttf` does until then (family `FreeSans`, weight bold, the face the shipped theme uses). Load fonts before the documents that use them. Each face costs a glyph atlas per size it is used at. A file that cannot be opened ends the script with `Unable to load` and the name.
|
|
|
|
*Parameters:*
|
|
|
|
* `filename` -- the font file, through the virtual file system like `fontLoad`.
|
|
* `fallback` -- `true` to make this the fallback face; `false` (the default) for an ordinary face.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guiload,guiLoad>>, <<fontload,fontLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The game's display face for headings; the shipped FreeSans stays for body 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)
|
|
----
|
|
|
|
Makes a GUI `width` by `height` pixels, rendered into its own texture every frame whether or not it is drawn, and returns its handle. The size is the document's coordinate space: `100%` of the body is the whole texture, and a `dp` is one of its pixels. A GUI the size of the overlay suits a full screen page; a HUD element or a monitor in the scene wants a smaller one, and a Raspberry Pi wants everything modest. A new GUI only displays until `guiSetInput` turns input on. Raises `The size must be positive` for a zero or negative size, and `No GUI available` when sixteen are already in use or the machine has no GPU device (a GUI needs the same device as the 3D scene).
|
|
|
|
*Parameters:*
|
|
|
|
* `width` -- the texture's width in pixels.
|
|
* `height` -- the texture's height in pixels.
|
|
|
|
*Returns:* The GUI handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guiload,guiLoad>>, <<guidraw,guiDraw>>, <<guidelete,guiDelete>>, <<materialsetgui,materialSetGui>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A page the size of the overlay, drawn over the disc.
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
|
doc = guiLoad(gui, DIR .. "gui/attract.rml")
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
guiDraw(gui)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#guisethandler]
|
|
==== guiSetHandler
|
|
|
|
[source,text]
|
|
----
|
|
guiSetHandler(gui, document, id, event, function)
|
|
guiSetHandler(gui, document, id, event, nil)
|
|
----
|
|
|
|
Calls `function(gui, document, id, event, value)` whenever the element with that `id` fires the named event: `"click"` for buttons and rows, `"change"` for form controls, `"submit"` for a form, or any other RmlUi event name (`"focus"`, `"mouseover"`, `"keydown"`, ...). `value` is the element's value at that moment as a string, as `guiGetValue` would return it (a range comes as RmlUi formats it, `"85.000000"`, so pass it through `tonumber`). One function is kept per element and event; setting another replaces it, and `nil` removes it. Handlers run as the events happen, between the game's own callbacks, and an error in one ends the game like an error in any callback. Ids are at most 63 characters. Raises `No element "id" in document N of GUI G` when the element does not exist, and `Argument 5 must be a function or nil` for anything else in that place.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- the element's `id` attribute.
|
|
* `event` -- the RmlUi event name.
|
|
* `function` -- the Lua function to call, or `nil` to stop calling one.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guigetvalue,guiGetValue>>, <<guisetvalue,guiSetValue>>, <<guisetinput,guiSetInput>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A volume slider takes effect as it moves; Start reads the rest of the form.
|
|
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)
|
|
----
|
|
|
|
Whether the mouse and light gun, keys, typed text and the framework switches from a pad or mouse reach the GUI. A new GUI only displays, which suits a HUD or a sign; turn input on for a menu or a form. While it is on, the pointer reaches the GUI wherever it is drawn flat (the last drawn on top) or shown on a surface in the scene, every key goes to it, a text field with focus turns on SDL's text input, and the pad's directions arrive as arrow keys with `ACTION_1` as Return and `ACTION_2` as Escape. What an element uses never reaches `onInputPressed`, `onKeyPressed` or the mouse callbacks; what nothing used falls through to them as usual. Raises an error for a handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guisethandler,guiSetHandler>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The HUD never takes input; the pause page does while it is up.
|
|
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)
|
|
----
|
|
|
|
Sets the value of the element with that `id`: a form control's value (a text field's text, a range's number, a `select` option's `value`; a checkbox's `checked` state is an attribute, set through RmlUi's API), or, for any other element, its inner RML, so a paragraph or a `div` can be rewritten with plain text or with markup. The document lays itself out again on the next frame. Raises `No element "id" in document N of GUI G` when the element does not exist.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- the element's `id` attribute.
|
|
* `value` -- the new value, a string; numbers are converted.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guigetvalue,guiGetValue>>, <<guisethandler,guiSetHandler>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The score line and the lives row are rewritten every frame; the row is 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)
|
|
----
|
|
|
|
Shows a document hidden by `guiHide` (a document is shown when `guiLoad` returns, so the call is not needed then). A shown document is drawn wherever its GUI is and takes input when its GUI does. Raises `No document N in GUI G` for a handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guihide,guiHide>>, <<guiload,guiLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The game over page comes up when the last life goes.
|
|
function onLastLifeLost()
|
|
guiSetValue(gui, gameOver, "final", string.format("%08d", score))
|
|
guiShow(gui, gameOver)
|
|
end
|
|
----
|
|
|
|
[#joint]
|
|
=== Joint
|
|
|
|
Joints connect two bodies, or a body and the world when the second node is `-1`, and hand back an integer joint handle that the other calls take. Anchors and axes are given in world space at the moment the joint is made, with the bodies where they are then, so place the nodes and give them bodies before jointing them. Distances are in world units and hinge angles in degrees. Deleting either body, or its node, deletes the joint; a handle that has been deleted raises an error. See <<physics,Physics>> in Game Development, Joints.
|
|
|
|
[#jointhinge]
|
|
==== jointHinge
|
|
|
|
[source,text]
|
|
----
|
|
joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz)
|
|
----
|
|
|
|
Makes a hinge between the two bodies through the world point `(ax, ay, az)` along the axis `(dx, dy, dz)`: a door, a wheel, a lever, a seesaw. The bodies keep that point and axis in common and may only turn about it. The axis is normalized, so its length does not matter; a zero axis becomes `(0, 1, 0)`. Both nodes must carry a body, or `nodeB` may be `-1` to hinge `nodeA` to the world. Raises an error when a node has no body or the joint cannot be made.
|
|
|
|
*Parameters:*
|
|
|
|
* `nodeA` -- the body whose turn is measured; a positive `jointSetLimits` angle is its right-hand turn about the axis.
|
|
* `nodeB` -- the other body, or `-1` for the world.
|
|
* `ax`, `ay`, `az` -- the anchor, a world point both bodies pass through the hinge at.
|
|
* `dx`, `dy`, `dz` -- the hinge axis in world space.
|
|
|
|
*Returns:* The joint handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<jointsetlimits,jointSetLimits>>, <<jointdelete,jointDelete>>, <<jointball,jointBall>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A door hinged to the world along its left edge, opening 100 degrees one way.
|
|
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)
|
|
-- Later, a shove near the handle swings it open.
|
|
bodyApplyImpulse(door, 0, 0, -60, 0.5, -0.62, 1.45)
|
|
----
|
|
|
|
[#jointball]
|
|
==== jointBall
|
|
|
|
[source,text]
|
|
----
|
|
joint = jointBall(nodeA, nodeB, ax, ay, az)
|
|
----
|
|
|
|
Makes a ball-and-socket joint at the world point `(ax, ay, az)`: the bodies may turn any way about it but cannot separate. A pendulum, a chain link, a hanging sign, a ragdoll shoulder. Both nodes must carry a body, or `nodeB` may be `-1` to hang `nodeA` from a fixed point in the world. A ball joint has no limits, so `jointSetLimits` refuses it. Raises an error when a node has no body or the joint cannot be made.
|
|
|
|
*Parameters:*
|
|
|
|
* `nodeA` -- the first body.
|
|
* `nodeB` -- the other body, or `-1` for the world.
|
|
* `ax`, `ay`, `az` -- the anchor, the world point the bodies pivot about.
|
|
|
|
*Returns:* The joint handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<jointhinge,jointHinge>>, <<jointdelete,jointDelete>>, <<bodyapplyimpulse,bodyApplyImpulse>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A chain of four links hanging from a hook, each balled to the one above.
|
|
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)
|
|
----
|
|
|
|
Lets the bodies slide relative to each other only along the axis `(dx, dy, dz)` through the anchor `(ax, ay, az)`, keeping their orientation: a drawer, a piston, a lift, a sliding door. The axis is normalized, so its length does not matter; a zero axis becomes `(0, 1, 0)`. Both nodes must carry a body, or `nodeB` may be `-1` to run `nodeA` along a rail fixed in the world. Raises an error when a node has no body or the joint cannot be made.
|
|
|
|
*Parameters:*
|
|
|
|
* `nodeA` -- the body whose travel is measured; a positive `jointSetLimits` distance is along the axis.
|
|
* `nodeB` -- the other body, or `-1` for the world.
|
|
* `ax`, `ay`, `az` -- a world point on the rail.
|
|
* `dx`, `dy`, `dz` -- the direction of travel in world space.
|
|
|
|
*Returns:* The joint handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<jointsetlimits,jointSetLimits>>, <<jointhinge,jointHinge>>, <<bodysetvelocity,bodySetVelocity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A crate on a rail along X, pushed sideways at the start and stopped 2.4 units on.
|
|
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)
|
|
----
|
|
|
|
Bounds a hinge's angle in degrees, or a slider's travel in world units, either side of where the joint started. A positive hinge angle is a right-hand turn of `nodeA` about the axis; a positive slider distance is along the axis. `low` is clamped to at most `0` and `high` to at least `0`, so the starting pose is always inside the range. Call it again to change the range at any time. Raises an error for a ball joint or a handle that no longer exists.
|
|
|
|
*Parameters:*
|
|
|
|
* `joint` -- the handle from `jointHinge` or `jointSlider`.
|
|
* `low` -- the farthest negative angle or distance, `0` or below.
|
|
* `high` -- the farthest positive angle or distance, `0` or above.
|
|
|
|
*Notes:* The two bodies swap roles if they are given the other way round, which mirrors the sign of the range.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<jointhinge,jointHinge>>, <<jointslider,jointSlider>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A lever that rocks 30 degrees each way, then locks once the puzzle is solved.
|
|
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)
|
|
----
|
|
|
|
Removes the joint; the bodies are free of each other again and keep whatever velocity they had. Joints also go away with either of their bodies, so this is only needed to free two bodies that both stay. Raises an error for a handle that does not exist or was already deleted.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<jointhinge,jointHinge>>, <<jointball,jointBall>>, <<bodydelete,bodyDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The rope snaps when the crate hanging from it is hit hard enough.
|
|
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
|
|
|
|
Every controller Singe opens is a gamepad and is read through the `controller*`
|
|
calls; a device SDL does not recognise reaches them through a mapping Singe
|
|
writes for it, so there is nothing else to call. See <<devices,Every Device Is
|
|
a Gamepad>>. The two calls here are about letting a stick stand in for a mouse.
|
|
|
|
[#joymouseenable]
|
|
==== joyMouseEnable
|
|
|
|
[source,text]
|
|
----
|
|
joyMouseEnable(enabled)
|
|
----
|
|
|
|
Hypseus extension. Turns joystick-to-mouse on or off while the game runs. With it on, the left stick of gamepad slot `0` drives the pointer the game sees, through the same `onMouseMoved` a real mouse reaches, held inside the picture; `--js_range` is the speed and the axis obeys its own dead zone. It is what makes a light gun game playable on a cabinet with a stick and no mouse.
|
|
|
|
It starts from `--joymouse`, which is off by default, and a `singeReload` puts it back there. Singe differs from Hypseus in that default: the shipped `controls.cfg` already binds that stick to the four direction switches, and driving the cursor as well without being asked would surprise every existing game.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00 (Hypseus)
|
|
*See also:* <<joymouseisenabled,joyMouseIsEnabled>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A gun game that lets the player aim with the stick when no mouse is plugged in.
|
|
if mouseHowMany() == 0 then
|
|
joyMouseEnable(true)
|
|
end
|
|
----
|
|
|
|
[#joymouseisenabled]
|
|
==== joyMouseIsEnabled
|
|
|
|
[source,text]
|
|
----
|
|
enabled = joyMouseIsEnabled()
|
|
----
|
|
|
|
Whether the stick is driving the mouse cursor right now, whether that was decided by `--joymouse` or by `joyMouseEnable`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<joymouseenable,joyMouseEnable>>
|
|
|
|
[#keyboard]
|
|
=== Keyboard
|
|
|
|
Singe reads the keyboard in one of two modes chosen with `keyboardSetMode`. In `MODE_NORMAL` (the default) keys are translated through the mappings in `controls.cfg` into the `SWITCH_*` values that reach `onInputPressed` and `onInputReleased`, and unmapped keys are ignored; in `MODE_FULL` every key reaches the script raw, through `onKeyPressed` and `onKeyReleased` as well as `onInputPressed` and `onInputReleased`. Whatever the mode, `keyboardIsDown`, `keyboardGetLastDown` and `keyboardGetLastUp` poll the live keyboard state, which is what the threaded model wants (see <<threaded,Threaded>>). A scancode names a physical key position (the `A` key is scancode `4` on every layout) while a keysym is the character the key produces; the `SCANCODE` table in `Framework.singe` names every scancode, `keyboardIsDown` takes scancodes, and `onKeyPressed` delivers both. The callbacks themselves are described under <<enginecallbacks,Engine Callbacks>>.
|
|
|
|
[#keyboardcatchquit]
|
|
==== keyboardCatchQuit
|
|
|
|
[source,text]
|
|
----
|
|
keyboardCatchQuit(catch)
|
|
----
|
|
|
|
Hypseus extension. Hands the quit switch to the script, exactly as `singeSetQuitKeyEnabled(not catch)` does: `keyboardCatchQuit(true)` stops the engine quitting on `INPUT_QUIT`, and `false` returns it. It is a function defined in `Framework.singe`, kept so games written for Hypseus Singe run unchanged.
|
|
|
|
*Notes:* Legacy alias.
|
|
|
|
WARNING: New code should call <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>, <<singequit,singeQuit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hypseus game code; the modern spelling is 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()
|
|
----
|
|
|
|
Returns the scancode of the key pressed most recently in the current frame, or `0` when nothing has been pressed since the last frame. The value is cleared after every `onOverlayUpdate` call, which makes it a one-shot "was a key just pressed" poll for the threaded model. Controller and mouse buttons are recorded here too, as their `GAMEPAD_N` and `MOUSE_N` codes, since the engine routes them through the same path as keys. The value is kept regardless of keyboard mode and of the `controls.cfg` mappings.
|
|
|
|
*Returns:* integer scancode, or `0`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<keyboardgetlastup,keyboardGetLastUp>>, <<keyboardisdown,keyboardIsDown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Threaded model: react to one key press per frame.
|
|
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()
|
|
----
|
|
|
|
Returns the scancode of the key released most recently in the current frame, or `0` when nothing has been released since the last frame. It is cleared after every `onOverlayUpdate` call, like `keyboardGetLastDown`, and records controller and mouse button codes the same way.
|
|
|
|
*Returns:* integer scancode, or `0`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<keyboardisdown,keyboardIsDown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A charged shot fires when the fire key is let go.
|
|
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()
|
|
----
|
|
|
|
Returns the keyboard mode in effect: `MODE_NORMAL` (`0`) or `MODE_FULL` (`1`). Every script starts in `MODE_NORMAL`, including after a reload.
|
|
|
|
*Returns:* integer mode.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<keyboardsetmode,keyboardSetMode>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Shared text-entry helper that restores whatever mode the caller had.
|
|
function promptForName()
|
|
local previous = keyboardGetMode()
|
|
keyboardSetMode(MODE_FULL)
|
|
entering = true
|
|
restoreMode = previous
|
|
end
|
|
----
|
|
|
|
[#keyboardgetmodifiers]
|
|
==== keyboardGetModifiers
|
|
|
|
[source,text]
|
|
----
|
|
modifiers = keyboardGetModifiers()
|
|
----
|
|
|
|
Returns the modifier keys held right now as a bitmask of SDL `KMOD_*` bits. Test it with a bitwise and against the values in the `MODIFIER` table: the single-key entries such as `MODIFIER.LSHIFT` and the combined `MODIFIER.SHIFT`, `MODIFIER.CTRL`, `MODIFIER.ALT` and `MODIFIER.GUI`, which cover either side. It reads the live state, so it works in every keyboard mode and from any callback.
|
|
|
|
*Returns:* integer bitmask.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<onkeypressedonkeyreleased,onKeyPressed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Shift+Tab toggles the statistics overlay.
|
|
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)
|
|
----
|
|
|
|
Tests whether a key is physically held right now, from the state the engine keeps for every key event it sees. It does not depend on the keyboard mode or on `controls.cfg`, and it works from any callback, so it suits movement that should continue while a key is held. Scancodes outside the range SDL defines return `false` rather than aborting.
|
|
|
|
*Parameters:*
|
|
|
|
* `scancode` -- a value from the `SCANCODE` table, such as `SCANCODE.LEFT.value`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Eight-way movement from held keys.
|
|
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)
|
|
----
|
|
|
|
Switches between the two keyboard models. In `MODE_NORMAL` only keys mapped in `controls.cfg` reach the script, as `SWITCH_*` values, key repeat is dropped so each press arrives once, and the engine acts on its own switches (pause, quit, screenshot, mouse grab). In `MODE_FULL` every key reaches `onInputPressed` and `onInputReleased` as its keysym and `onKeyPressed` and `onKeyReleased` as keysym and scancode, key repeat is delivered for text entry, and keyboard mappings of the engine's own switches are ignored so that the game keeps every key; controller and mouse button mappings still act. Most games set the mode once at startup; a game with text entry switches to `MODE_FULL` for the duration and back. Any other value aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `mode` -- `MODE_NORMAL` (`0`) or `MODE_FULL` (`1`).
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<keyboardgetmode,keyboardGetMode>>, <<oninputpressedoninputreleased,onInputPressed>>, <<onkeypressedonkeyreleased,onKeyPressed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- High score entry takes raw keys, then gameplay mode comes back.
|
|
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]
|
|
=== Light
|
|
|
|
Lights are nodes carrying a light, made by `lightNew` and moved, parented and animated with the node calls. Up to eight visible lights shine in a frame, in node order; `sceneSetAmbient` and a sky from `sceneSetSky` add light from everywhere. Directional and spot lights shine down the node's -Z, so aim them with `nodeLookAt` or `nodeSetRotation`; point lights only need a position. Colors are integers from `0` to `255`, intensities are multipliers in the scene's linear light, angles are degrees, and distances are world units. Every `lightSet*` call raises an error for a node that does not carry a light. See <<scenes3d,3D Scenes>> for lighting, exposure and shadows.
|
|
|
|
[#lightnew]
|
|
==== lightNew
|
|
|
|
[source,text]
|
|
----
|
|
node = lightNew(type)
|
|
node = lightNew(type, parent)
|
|
----
|
|
|
|
Makes a new node carrying a light and returns the node handle. The light starts white at intensity `1`, with no range limit and, for a spot, a cone of `20` to `30` degrees. Place and aim it with the node calls: `nodeSetPosition` for a point or spot light, `nodeLookAt` for a directional or spot light. A light parented to a moving node moves with it, so a headlight on a car or a torch in a hand is one call. Raises an error for an unknown type or when no node can be made.
|
|
|
|
*Parameters:*
|
|
|
|
* `type` -- `LIGHT_DIRECTIONAL` (a sun; only its direction matters), `LIGHT_POINT` (a bulb fading with distance in every direction), or `LIGHT_SPOT` (a cone down the node's -Z).
|
|
* `parent` -- the parent node; the root when omitted.
|
|
|
|
*Returns:* The light's node handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightsetcolor,lightSetColor>>, <<lightsetintensity,lightSetIntensity>>, <<lightsetrange,lightSetRange>>, <<lightsetcone,lightSetCone>>, <<scenesetambient,sceneSetAmbient>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Warm key light from the upper left, a cool fill from the right,
|
|
-- and a headlight that rides with the car.
|
|
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)
|
|
----
|
|
|
|
The light's color, `0` to `255` per channel (out-of-range values are clamped), treated as sRGB; white by default. Brightness belongs in `lightSetIntensity`, so keep the color for the hue (a warm `255, 230, 190` bulb, a cool `150, 190, 255` moon) and leave at least one channel near `255`. Raises an error when the node carries no light.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightsetintensity,lightSetIntensity>>, <<lightnew,lightNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The neon sign cycles through its colors.
|
|
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)
|
|
----
|
|
|
|
The shape of a spot light's cone, both angles measured from the axis (the node's -Z): full brightness within `innerDegrees`, fading smoothly to nothing at `outerDegrees`. The defaults are `20` and `30`. A narrow gap between the two gives a hard-edged theater spot, a wide one a soft pool. The values are stored for every light but only a `LIGHT_SPOT` uses them. Raises an error when the node carries no light.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the light's node.
|
|
* `innerDegrees` -- half angle of full brightness, in degrees from the axis.
|
|
* `outerDegrees` -- half angle where the light reaches nothing, in degrees from the axis; raised to `innerDegrees` when smaller.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightnew,lightNew>>, <<lightsetrange,lightSetRange>>, <<nodelookat,nodeLookAt>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A stage spot on the singer, hard edged, from the lighting rig.
|
|
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)
|
|
----
|
|
|
|
The light's brightness multiplier in the scene's linear light, default `1`. A directional light of intensity `1` lights a white surface facing it to white. A point or spot light fades with distance as `1 / (1 + d^2)`, so its intensity is the brightness at the light itself, half of it one unit away and a hundredth ten units away; bulbs usually want values well above `1`, `10` to `50` for a room, and `sceneSetExposure` brings the whole scene back into range. Raises an error when the node carries no light.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightsetrange,lightSetRange>>, <<scenesetexposure,sceneSetExposure>>, <<lightsetcolor,lightSetColor>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A lantern that flickers in the wind.
|
|
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)
|
|
----
|
|
|
|
How far a point or spot light reaches, in world units; `0` (the default) means no limit. The light fades to exactly nothing at the range, on top of its distance falloff, so lights do not pop as objects cross it. A range also bounds how far a shadow-casting light's shadow map has to reach, which keeps its shadows sharp. Directional lights ignore it. Raises an error when the node carries no light.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightsetintensity,lightSetIntensity>>, <<lightsetshadow,lightSetShadow>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Street lamps down the road, each lighting its own stretch.
|
|
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)
|
|
----
|
|
|
|
Makes the light cast shadows from everything opaque in the scene, or stops it; off by default. Any of the eight lights may cast, in any mix. A spot light gets one shadow map; a directional light gets one map per cascade (`sceneSetShadowCascades`); a point light gets a cube map of six views, which covers every direction (a bulb inside a room) at six times the cost. `nodeSetShadow` excuses a mesh from casting. Raises an error when the node carries no light.
|
|
|
|
*Notes:* Each shadow map costs one extra pass over the scene per frame, a cube map six, and a cascaded sun as many as it has cascades; the maps' size is set by `sceneSetShadowSize`. A modern integrated GPU manages eight casters comfortably; a Raspberry Pi wants one or two at `512`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetshadowsize,sceneSetShadowSize>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<nodesetshadow,nodeSetShadow>>, <<lightnew,lightNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The sun casts shadows; the lamps only light. The bulb's own glass
|
|
-- must not shadow its lamp.
|
|
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
|
|
|
|
A material is how a mesh looks: a base color with alpha, an optional base texture from a sprite, a KTX2 file, a video or a rendered view, and the glTF metallic-roughness parameters with four optional maps. `materialNew` returns an integer handle; pass it to `nodeSetMesh` or `nodeSetMaterial`, and to every `materialSet*` call. Materials are shared, so changing one changes every mesh drawn with it, and a node without one draws white and half rough. Colors are integers from `0` to `255` per channel and are treated as sRGB; factors such as roughness run from `0` to `1`. A bad handle raises an error. See <<scenes3d,3D Scenes>> for how materials are lit.
|
|
|
|
[#materialnew]
|
|
==== materialNew
|
|
|
|
[source,text]
|
|
----
|
|
material = materialNew()
|
|
----
|
|
|
|
Allocates a new material and returns its handle. It starts white and opaque (color `255, 255, 255, 255`), with roughness `0.5`, metallic `0`, no emissive color, tiling `1, 1`, linear filtering, single sided, lit, and no texture or maps. Set what differs from that and hand the handle to `nodeSetMesh`. Loaded models bring their own materials, so this is for meshes built from script. Raises an error on a machine with no usable GPU.
|
|
|
|
*Returns:* The material handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>, <<materialdelete,materialDelete>>, <<nodesetmesh,nodeSetMesh>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Brushed gold for a trophy, translucent blue glass for its case.
|
|
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)
|
|
----
|
|
|
|
Frees the material and any texture it uploaded. Every node that was drawn with it falls back to the default look (white, half rough), so reassign those nodes with `nodeSetMaterial` first if they should keep a look. The handle is invalid afterwards and raises an error if used again. The private material a node makes for itself under `nodeSetSprite` or `nodeSetText` belongs to the node and cannot be deleted; the call raises an error, and clearing the sprite or text frees it. Materials are cheap; deleting them matters mostly for ones that carried large textures.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Swap the level's ground look when the season changes.
|
|
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)
|
|
----
|
|
|
|
When `true` the material is drawn translucent, blending by the base color's alpha multiplied by the texture's alpha, after every opaque mesh and sorted farthest first. Blended meshes do not write depth, so leave it `false` (the default) for solid surfaces: an opaque mesh with blending on can show things behind it drawing through. Use it with an alpha below `255` from `materialSetColor` or a texture with transparency.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A force field that fades in when it is hit.
|
|
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)
|
|
----
|
|
|
|
Sets the base color, `0` to `255` per channel (out-of-range values are clamped), with `a` defaulting to `255`. The color is treated as sRGB and lit in linear light. When the material has a texture, video or view, the color multiplies it, so white leaves the picture as is and a darker color tints it. The alpha only shows through with `materialSetBlend`.
|
|
|
|
*Parameters:*
|
|
|
|
* `material` -- the material handle.
|
|
* `r, g, b` -- red, green and blue, `0` to `255`.
|
|
* `a` -- opacity, `0` to `255`; `255` when omitted.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsettexture,materialSetTexture>>, <<materialsetblend,materialSetBlend>>, <<materialsetemissive,materialSetEmissive>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Team colors on the one car 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)
|
|
----
|
|
|
|
Draws both faces of every triangle instead of culling the back face. Needed for open shapes seen from behind, such as a single `meshPlane` viewed from below, a flag, or a leaf card. Closed shapes look the same either way, so leave it `false` (the default) for them: a double-sided material costs fill rate and shadows are cast from both faces too.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshplane,meshPlane>>, <<materialsetblend,materialSetBlend>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A banner hanging over the street, readable from both sides.
|
|
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)
|
|
----
|
|
|
|
The light the surface gives off on its own, `0` to `255` per channel (out-of-range values are clamped), added on top of the lighting: screens, lamp bulbs, neon, instrument panels. The default is black, no glow. It lights nothing else and casts no light; put a light node at the same place for that. With `materialSetEmissiveMap` the map says where on the surface this color applies. Emissive surfaces bright enough to pass the threshold of `sceneSetBloom` glow.
|
|
|
|
*Notes:* An unlit material (`materialSetUnlit`) shows its base color only; the emissive color is ignored on it.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetemissivemap,materialSetEmissiveMap>>, <<materialsetunlit,materialSetUnlit>>, <<scenesetbloom,sceneSetBloom>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A warning lamp that pulses red when the reactor overheats.
|
|
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)
|
|
----
|
|
|
|
A texture saying which parts of the surface glow. Its color is multiplied by the color from `materialSetEmissive`, so set that too: white to use the map's own colors, or a tint. The image comes from a loaded sprite (a copy is uploaded, so the sprite may be unloaded afterwards) or from a `.ktx2` file by name, kept compressed on the GPU. With `nil` or no second argument the map is removed. Raises an error when the texture cannot be uploaded or the file is not a KTX2 texture.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetemissive,materialSetEmissive>>, <<materialsettexture,materialSetTexture>>, <<materialsettiling,materialSetTiling>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A control panel whose buttons and readouts light up.
|
|
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)
|
|
----
|
|
|
|
How the material's textures are sampled. `FILTER_LINEAR` (the default) blends between texels and between mipmap levels, with anisotropic filtering on surfaces seen at a grazing angle. `FILTER_NEAREST` picks the closest texel and the closest mipmap level, so pixel art stays crisp on a quad. Every texture carries a full mipmap chain, so a texture seen small never sparkles either way. Any other value raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsettexture,materialSetTexture>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A 16 by 16 pixel tile drawn on a block without smearing.
|
|
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)
|
|
----
|
|
|
|
Shows a GUI from `guiNew` as the material's base color texture, updated every frame: a monitor, a cockpit panel, a sign, with the same document a `guiDraw` would show flat. Usually paired with `materialSetUnlit` so the page is not shaded by the lights, and tinted by the base color like any texture. With `nil` or no second argument the material goes back to its own texture, which is kept underneath; deleting the GUI does the same. Setting a GUI drops any video or view the material was showing. While the GUI takes input, the mouse or a light gun pointing at a surface drawn with the material reaches it through the same probe as `sceneProbeGui`. Raises an error for a GUI handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guinew,guiNew>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>, <<materialsetview,materialSetView>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The options page on the screen of the modeled cabinet.
|
|
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)
|
|
----
|
|
|
|
How metallic the surface is, `0` for dielectrics (paint, plastic, wood, skin) to `1` for metals, which tint their highlights and reflections with the base color. The default is `0`. Values between are for blends and worn surfaces; most materials want one end or the other. With `materialSetMetallicRoughnessMap` the map's blue channel multiplies this value. A metal has little to reflect without a sky from `sceneSetSky`, so pair the two.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetroughness,materialSetRoughness>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>, <<scenesetsky,sceneSetSky>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Chrome bumper, painted body.
|
|
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)
|
|
----
|
|
|
|
A texture varying the finish across the surface: its green channel multiplies the roughness from `materialSetRoughness` and its blue channel the metallic value from `materialSetMetallic`, the way glTF packs them, so a map from a glTF tool works as is. Set both factors to `1` to use the map's values alone. The image comes from a sprite or a `.ktx2` file by name; `nil` or no second argument removes the map. Raises an error when the texture cannot be uploaded.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetroughness,materialSetRoughness>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Scratched metal plating: shiny where the paint has worn off.
|
|
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)
|
|
----
|
|
|
|
A tangent-space normal map (the usual bluish kind, flat at `128, 128, 255`) that tilts the lighting per texel so bricks, grooves and rivets catch the light without extra geometry. Every mesh has tangents for it: a model's own when the file carries them, otherwise computed from the UVs. The image comes from a sprite or a `.ktx2` file by name; `nil` or no second argument removes the map. Raises an error when the texture cannot be uploaded.
|
|
|
|
*Parameters:*
|
|
|
|
* `material` -- the material handle.
|
|
* `sprite` or `"file.ktx2"` -- the map, from a loaded sprite or a KTX2 file; `nil` or omitted removes it.
|
|
* `strength` -- how far the bumps tilt the lighting, `1` (the default) as authored, `0` flat, above `1` exaggerated.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsettexture,materialSetTexture>>, <<materialsetocclusionmap,materialSetOcclusionMap>>, <<materialsettiling,materialSetTiling>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A brick wall from a flat plane, its mortar lines catching the low sun.
|
|
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)
|
|
----
|
|
|
|
[#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)
|
|
----
|
|
|
|
An ambient occlusion map, occlusion in the red channel (white for open, dark for crevices), that darkens the ambient and sky light in corners and folds. Direct light from light nodes is not affected, as in glTF. The image comes from a sprite or a `.ktx2` file by name; `nil` or no second argument removes the map. Raises an error when the texture cannot be uploaded.
|
|
|
|
*Parameters:*
|
|
|
|
* `material` -- the material handle.
|
|
* `sprite` or `"file.ktx2"` -- the map, from a loaded sprite or a KTX2 file; `nil` or omitted removes it.
|
|
* `strength` -- how much of the map to apply, `0` none to `1` (the default) fully.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetnormalmap,materialSetNormalMap>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetenvironment,sceneSetEnvironment>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Baked shading for the cave walls, half strength so the lamps still reach the corners.
|
|
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)
|
|
----
|
|
|
|
How rough the surface is, `0` for a mirror-tight highlight to `1` for matte. The default is `0.5`. Highlights follow the GGX model, so low values give a small bright spot and sharp sky reflections, high values a broad soft sheen and blurred ones. With `materialSetMetallicRoughnessMap` the map's green channel multiplies this value.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Wet the road when it rains: the same asphalt, tighter highlights.
|
|
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)
|
|
----
|
|
|
|
Uses a sprite's image as the base color texture (a copy is uploaded, so the sprite may be unloaded afterwards), or a KTX2 file by name, kept compressed on the GPU in the block format the machine has (see Compressed textures in <<scenes3d,3D Scenes>>). With `nil` or no second argument the texture is removed and the material shows its base color alone. Setting a texture also drops any video or view the material was showing. Texture coordinates come from the mesh; the primitives map an image once across each face, and `materialSetTiling` repeats it. A name that is not a KTX2 file raises an error (load other pictures with `spriteLoad`), as does a failed upload.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<spriteload,spriteLoad>>, <<materialsettiling,materialSetTiling>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A crate textured from a sprite the game already loads for the menu,
|
|
-- and a floor from a compressed texture shipped with the game.
|
|
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)
|
|
----
|
|
|
|
How many times the material's textures (base and maps alike) repeat across a surface's `0` to `1` UV range, `1` and `1` by default. `24, 24` on a heightmap 120 units across tiles a texture every five units. Textures repeat past their edges, so the picture needs to tile cleanly. Values below `1` show part of the image; negative values mirror it.
|
|
|
|
*Parameters:*
|
|
|
|
* `material` -- the material handle.
|
|
* `u` -- repeats across the surface's U direction.
|
|
* `v` -- repeats across the surface's V direction.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsettexture,materialSetTexture>>, <<meshheightmap,meshHeightmap>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Grass every five units across the hills.
|
|
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)
|
|
----
|
|
|
|
Shows the base color and texture exactly as they are, ignoring every light, the ambient and the sky: for video screens, monitors showing a view, skyboxes, and flat-shaded looks. Fog still applies. The emissive color and the maps are not used on an unlit material. The default is `false`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetemissive,materialSetEmissive>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The arcade cabinet's marquee: a picture that is lit from inside, so the
|
|
-- room's lights should not shade it.
|
|
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)
|
|
----
|
|
|
|
Shows a rendered view from `viewNew` as the material's base color texture, updated every frame: a security monitor, a rear-view mirror, a portal. Usually paired with `materialSetUnlit` so the picture is not shaded by the lights, and tinted by the base color like any texture. With `nil` or no second argument the material goes back to its own texture, which is kept underneath. Setting a view drops any video the material was showing. Raises an error for a view handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<viewnew,viewNew>>, <<viewsetcamera,viewSetCamera>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A rear-view mirror on the player's car.
|
|
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)
|
|
----
|
|
|
|
Uses the laserdisc (no `video`) or a video loaded with `videoLoad` as the base color texture, updated every frame while it plays. Usually paired with `materialSetUnlit` so the picture is not tinted by the lights. The video keeps playing whether or not it is also drawn on the overlay, and several materials may show the same one. Setting a video drops any texture or view the material had; `materialSetTexture` takes it off again.
|
|
|
|
*Notes:* The disc form raises an error in a game without a disc; a bad video handle raises one too.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<videoload,videoLoad>>, <<materialsetunlit,materialSetUnlit>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A modeled cabinet whose screen shows the laserdisc, and a wall of
|
|
-- monitors looping an attract clip.
|
|
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
|
|
|
|
A mesh is geometry on the GPU: a primitive the engine builds, a terrain
|
|
from an image, or vertex data from your script. Handles are integers
|
|
returned by the `mesh*` constructors; any number of nodes can share one
|
|
mesh, and nodes drawing the same mesh with the same material draw as one
|
|
instanced call, so make one mesh and many nodes. Sizes are in scene units
|
|
and each primitive is centered on its node's origin unless stated
|
|
otherwise, so the node's transform places, turns, and scales it. Texture
|
|
coordinates run from `0` to `1` once across each face or surface. Every
|
|
constructor needs the 3D renderer described in <<scenes3d,3D Scenes>> and
|
|
aborts the script when it is unavailable.
|
|
|
|
[#meshbox]
|
|
==== meshBox
|
|
|
|
[source,text]
|
|
----
|
|
mesh = meshBox(width, height, depth)
|
|
----
|
|
|
|
A box `width` along X, `height` along Y, and `depth` along Z, centered on
|
|
the origin. Each of the six faces has its own four vertices with a flat
|
|
normal, so edges shade crisply, and the texture is laid once across every
|
|
face. Crates, walls, floors, and car bodies are all boxes.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmesh,nodeSetMesh>>, <<meshplane,meshPlane>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A stone floor slab and a stack of crates sharing one mesh.
|
|
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)
|
|
----
|
|
|
|
A cone standing on Y with a closed base: the base circle of `radius` at
|
|
`-height / 2` and the apex at `+height / 2`. `segments` (default `24`, at
|
|
least `3`) is how many facets go round it; the sides shade smoothly across
|
|
them. The texture wraps once round the sides and is laid once across the
|
|
base.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshcylinder,meshCylinder>>, <<nodesetmesh,nodeSetMesh>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A lampshade over the bulb, and a row of pine trees from two cones each.
|
|
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)
|
|
----
|
|
|
|
A cylinder along Y, closed at both ends, from `-height / 2` to
|
|
`+height / 2`. `segments` (default `24`, at least `3`) is how many facets
|
|
go round it, shaded smoothly. For a wheel or a coin, rotate the node `90`
|
|
degrees about X or Z so the axis lies where you need it.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshcone,meshCone>>, <<meshtorus,meshTorus>>, <<nodesetrotation,nodeSetRotation>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A lamp post, and a tire lying on its side as a wheel.
|
|
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)
|
|
----
|
|
|
|
Frees the mesh's GPU buffers and its CPU copy. Nodes that used it are
|
|
left with no mesh (and no morph weights) and draw nothing until given
|
|
another; the handle becomes invalid and may be reused by a later
|
|
constructor. Meshes are not freed when a node is deleted, so delete a mesh
|
|
yourself when nothing will use it again.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshnew,meshNew>>, <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Rebuild the track from a new heightmap between rounds.
|
|
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)
|
|
----
|
|
|
|
A terrain mesh from a grayscale image, `2` to `1025` pixels each way:
|
|
one vertex per pixel across `sizeX` by `sizeZ` units centered on the node,
|
|
each raised by the pixel's red channel (`0` black to `255` white) times
|
|
`sizeY`, with the image's top row at the far (`-Z`) edge. Normals are
|
|
computed from the slopes and texture coordinates run `0` to `1` across the
|
|
whole, so `materialSetTiling` sets the texture's repeat. The height samples
|
|
are kept: `terrainGetHeight` reads them, and a `SHAPE_MESH` body on the
|
|
node becomes a height field. A file that cannot be opened or decoded, the
|
|
wrong size, or no 3D renderer aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `image` -- the picture's game-relative name, any format `spriteLoad` reads.
|
|
* `sizeX` -- the terrain's width along X in world units.
|
|
* `sizeY` -- the height of a white pixel above a black one.
|
|
* `sizeZ` -- the terrain's depth along Z in world units.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<terraingetheight,terrainGetHeight>>, <<materialsettiling,materialSetTiling>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[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)
|
|
-- The dragon's feet on the slope, wherever the ground is there.
|
|
nodeSetPosition(dragon, -8, terrainGetHeight(hills, -8, 3) or 0, 3)
|
|
----
|
|
|
|
[#meshnew]
|
|
==== meshNew
|
|
|
|
[source,text]
|
|
----
|
|
mesh = meshNew(positions, normals, uvs, indices)
|
|
----
|
|
|
|
Builds geometry from tables of numbers. Tangents for normal mapping are
|
|
computed for you, and so are smooth normals when `normals` is `nil`, by
|
|
averaging the triangles that share each vertex. Triangles are front facing
|
|
when their vertices run counter-clockwise seen from outside; give the
|
|
material `materialSetDoubleSided` if the back must show. A table of the
|
|
wrong shape, or an index outside the vertex list, aborts the script with a
|
|
message saying which.
|
|
|
|
*Parameters:*
|
|
|
|
* `positions` -- `x, y, z` for every vertex, one after another; at least one vertex.
|
|
* `normals` -- `x, y, z` per vertex, the same count as `positions`, or `nil` to have them computed.
|
|
* `uvs` -- `u, v` per vertex (`0` to `1`, `v` down the image), or `nil` for none (every vertex at `0, 0`).
|
|
* `indices` -- three vertex numbers per triangle, counted from `1` as the vertices appear in `positions`; at least one triangle.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmesh,nodeSetMesh>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<meshdelete,meshDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A flat arrow pointing along +X, seen from above (double sided so it works from below too).
|
|
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)
|
|
----
|
|
|
|
A flat rectangle in the XZ plane facing +Y, `width` along X and `depth`
|
|
along Z, centered on the origin, with the texture laid once across it.
|
|
Two arguments give a single quad. `columns` (and `rows`, which defaults to
|
|
`columns`) subdivide it into that many quads each way, which a soft body
|
|
(`softNew`) needs to bend as cloth; both must be at least `1`. Rotate the
|
|
node `90` degrees about X to stand it up facing +Z. It is single sided
|
|
unless its material is double sided.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsetvideo,materialSetVideo>>, <<softnew,softNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The ground, a screen showing the disc, and a flag that will be cloth.
|
|
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)
|
|
----
|
|
|
|
A sphere of `radius`, centered on the origin, built from latitude and
|
|
longitude bands: `segments` (default `32`, at least `3`) around the
|
|
equator and half as many from pole to pole. The texture wraps once round
|
|
it, `v` from the top pole to the bottom. Fewer segments make a lighter
|
|
mesh for balls seen from afar; more make a smoother silhouette up close.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshtorus,meshTorus>>, <<nodesetmesh,nodeSetMesh>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A glass ball on the floor, and a small marker sphere that shares a look.
|
|
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)
|
|
----
|
|
|
|
A ring lying in the XZ plane around Y, centered on the origin: `radius`
|
|
from the center to the middle of the tube and `tubeRadius` the tube's own.
|
|
`segments` (default `32`, at least `3`) go around the ring and half as
|
|
many (at least `3`) around the tube. The texture wraps once round the ring
|
|
in `u` and once round the tube in `v`.
|
|
|
|
*Returns:* The mesh handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshsphere,meshSphere>>, <<meshcylinder,meshCylinder>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A gold ring standing on edge for the ball to roll through.
|
|
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)
|
|
----
|
|
|
|
[#model]
|
|
[#midi]
|
|
=== MIDI
|
|
|
|
Two unrelated things share this name, and it is worth keeping them apart.
|
|
|
|
A **MIDI file** is music, and it plays like any other music: hand `.mid` to
|
|
<<musicload,`musicLoad`>>. Singe synthesises it from a SoundFont, because a MIDI
|
|
file holds notes rather than sound. It ships no SoundFont -- a good one is tens
|
|
of megabytes for a format almost no game uses -- so name one with
|
|
`--soundfont`, put one at `Singe/soundfont.sf2` so a packed game carries its
|
|
own, or let Singe find the one your distribution installed. The `SoundFont:`
|
|
line of the trace header says which was used. With no SoundFont anywhere,
|
|
loading a `.mid` fails and says so.
|
|
|
|
**MIDI ports** are the calls below. They send and receive live messages on a
|
|
real port: a cabinet driving an external sound module, or a keyboard, a fader
|
|
box or a control surface read as another input device. Nothing here synthesises
|
|
anything, and nothing here touches a MIDI file.
|
|
|
|
Ports are numbered from `0`. Nothing is opened until a script first asks -- a
|
|
MIDI port costs a file descriptor and a round of driver configuration, and a
|
|
game that never mentions MIDI never pays for it -- so the first of these calls
|
|
is what takes stock of the machine, and <<midirescan,`midiRescan`>> looks again
|
|
for a device plugged in since. One input
|
|
port and one output port may be open at a time. Channels are `1` to `16` as a
|
|
person counts them, not `0` to `15` as the wire does, and every other value is
|
|
`0` to `127`. On a machine with no MIDI at all -- no ports, or no MIDI library
|
|
installed -- the counts are `0`, the open calls answer `false`, and the sending
|
|
calls answer `false`; nothing fails.
|
|
|
|
Linux reaches every port on the ALSA sequencer, which is both the hardware
|
|
interfaces and any synthesiser or other program registering one. macOS uses
|
|
CoreMIDI and Windows the multimedia MIDI calls, where output device `0` is
|
|
usually the synthesiser the system itself provides.
|
|
|
|
==== midiCloseInput
|
|
|
|
[source,text]
|
|
----
|
|
midiCloseInput()
|
|
----
|
|
|
|
Stops listening to the input port. <<onmidimessage,`onMidiMessage`>> stops firing. Closing a port that is not open does nothing.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiopeninput,midiOpenInput>>
|
|
|
|
==== midiCloseOutput
|
|
|
|
[source,text]
|
|
----
|
|
midiCloseOutput()
|
|
----
|
|
|
|
Releases the output port. The sending calls answer `false` from now on. Closing a port that is not open does nothing.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiopenoutput,midiOpenOutput>>
|
|
|
|
==== midiControlChange
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiControlChange(channel, control, value)
|
|
----
|
|
|
|
Moves a controller on the open output port: volume is controller `7`, pan is `10`, the sustain pedal is `64`.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- number; `1` to `16`.
|
|
* `control` -- number; `0` to `127`.
|
|
* `value` -- number; `0` to `127`.
|
|
|
|
*Returns:* `true` when the message went out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midisend,midiSend>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Duck the module's volume while a character speaks.
|
|
midiControlChange(1, 7, 40)
|
|
----
|
|
|
|
==== midiInputCount
|
|
|
|
[source,text]
|
|
----
|
|
count = midiInputCount()
|
|
----
|
|
|
|
How many MIDI input ports this machine has. `0` when there are none, or when the platform has no MIDI at all.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* a number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiinputname,midiInputName>>
|
|
|
|
==== midiInputName
|
|
|
|
[source,text]
|
|
----
|
|
name = midiInputName(index)
|
|
----
|
|
|
|
The name of an input port, for a settings screen that lets a player choose one.
|
|
|
|
*Parameters:*
|
|
|
|
* `index` -- number; `0` to `midiInputCount() - 1`.
|
|
|
|
*Returns:* a string, or `nil` when the index names no port.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiinputcount,midiInputCount>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
for i = 0, midiInputCount() - 1 do
|
|
printOverlay(midiInputName(i))
|
|
end
|
|
----
|
|
|
|
==== midiIsInputOpen
|
|
|
|
[source,text]
|
|
----
|
|
open = midiIsInputOpen()
|
|
----
|
|
|
|
Whether an input port is open.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiopeninput,midiOpenInput>>
|
|
|
|
==== midiIsOutputOpen
|
|
|
|
[source,text]
|
|
----
|
|
open = midiIsOutputOpen()
|
|
----
|
|
|
|
Whether an output port is open. Worth asking before a burst of messages, since each one would otherwise answer `false` separately.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiopenoutput,midiOpenOutput>>
|
|
|
|
==== midiNoteOff
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiNoteOff(channel, key)
|
|
----
|
|
|
|
Releases a note on the open output port. A note left on sounds until something stops it, so every `midiNoteOn` needs one of these.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- number; `1` to `16`.
|
|
* `key` -- number; `0` to `127`, where `60` is middle C.
|
|
|
|
*Returns:* `true` when the message went out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midinoteon,midiNoteOn>>
|
|
|
|
==== midiNoteOn
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiNoteOn(channel, key, velocity)
|
|
----
|
|
|
|
Sounds a note on the open output port.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- number; `1` to `16`. Channel `10` is percussion on a General MIDI device.
|
|
* `key` -- number; `0` to `127`, where `60` is middle C.
|
|
* `velocity` -- number; `0` to `127`, how hard the key was struck.
|
|
|
|
*Returns:* `true` when the message went out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midinoteoff,midiNoteOff>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A stinger on an external module when the player is hit.
|
|
midiNoteOn(10, 49, 110)
|
|
midiNoteOff(10, 49)
|
|
----
|
|
|
|
==== midiOpenInput
|
|
|
|
[source,text]
|
|
----
|
|
opened = midiOpenInput(index)
|
|
----
|
|
|
|
Listens to one input port. <<onmidimessage,`onMidiMessage`>> then fires for every message that arrives. Whichever input port was open is closed first.
|
|
|
|
*Parameters:*
|
|
|
|
* `index` -- number; `0` to `midiInputCount() - 1`.
|
|
|
|
*Returns:* `true` when the port opened.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midicloseinput,midiCloseInput>>, <<onmidimessage,onMidiMessage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
if midiInputCount() > 0 then
|
|
midiOpenInput(0)
|
|
end
|
|
----
|
|
|
|
==== midiOpenOutput
|
|
|
|
[source,text]
|
|
----
|
|
opened = midiOpenOutput(index)
|
|
----
|
|
|
|
Takes one output port for the sending calls. Whichever output port was open is closed first.
|
|
|
|
*Parameters:*
|
|
|
|
* `index` -- number; `0` to `midiOutputCount() - 1`.
|
|
|
|
*Returns:* `true` when the port opened.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midicloseoutput,midiCloseOutput>>
|
|
|
|
==== midiOutputCount
|
|
|
|
[source,text]
|
|
----
|
|
count = midiOutputCount()
|
|
----
|
|
|
|
How many MIDI output ports this machine has.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* a number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midioutputname,midiOutputName>>
|
|
|
|
==== midiOutputName
|
|
|
|
[source,text]
|
|
----
|
|
name = midiOutputName(index)
|
|
----
|
|
|
|
The name of an output port.
|
|
|
|
*Parameters:*
|
|
|
|
* `index` -- number; `0` to `midiOutputCount() - 1`.
|
|
|
|
*Returns:* a string, or `nil` when the index names no port.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midioutputcount,midiOutputCount>>
|
|
|
|
==== midiPitchBend
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiPitchBend(channel, value)
|
|
----
|
|
|
|
Bends a channel. `8192` is the middle, meaning no bend; how far `0` and `16383` reach is the receiving device's own setting, usually two semitones.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- number; `1` to `16`.
|
|
* `value` -- number; `0` to `16383`.
|
|
|
|
*Returns:* `true` when the message went out.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== midiProgramChange
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiProgramChange(channel, program)
|
|
----
|
|
|
|
Chooses the sound a channel plays. The numbers are the receiving device's; on a General MIDI device `0` is a grand piano.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- number; `1` to `16`.
|
|
* `program` -- number; `0` to `127`.
|
|
|
|
*Returns:* `true` when the message went out.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== midiRescan
|
|
|
|
[source,text]
|
|
----
|
|
midiRescan()
|
|
----
|
|
|
|
Looks for ports again, for a device plugged in while the game is running. An open port stays open. Indexes are only as stable as the list, so read the names again after this rather than assuming port `2` is still what it was.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>>
|
|
|
|
==== midiSend
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiSend(byte [, byte [, byte]])
|
|
sent = midiSend(string)
|
|
----
|
|
|
|
Any message at all, for the ones the named calls do not cover. Given numbers it sends them as they are, which is a status byte and up to two data bytes -- and note that the status byte carries the channel in its low four bits, counting from `0`, unlike every other call here. Given a string it sends the whole string, which is how a system exclusive message goes out.
|
|
|
|
*Parameters:*
|
|
|
|
* `byte` -- number; `0` to `255`, one to three of them.
|
|
* `string` -- a string of bytes, sent unchanged.
|
|
|
|
*Returns:* `true` when the message went out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midinoteon,midiNoteOn>>, <<midicontrolchange,midiControlChange>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- All notes off on channel 1, which has no named call.
|
|
midiSend(0xB0, 123, 0)
|
|
|
|
-- A system exclusive message, bytes and all.
|
|
midiSend(string.char(0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7))
|
|
----
|
|
|
|
=== Model
|
|
|
|
Models are glTF 2.0 binary files (`.glb`) loaded once and placed any number
|
|
of times. A model must be self-contained (buffers and images inside the
|
|
file); one referring to a file beside it is refused. Loading gives an integer
|
|
handle; `modelInstance` builds the model's nodes into the scene and returns
|
|
their root, which every `node*` and `animation*` call then takes. Positions
|
|
and sizes inside a model are in world units, as the file stores them; the
|
|
root node scales the whole instance. See <<scenes3d,3D Scenes>>.
|
|
|
|
[#modelload]
|
|
==== modelLoad
|
|
|
|
[source,text]
|
|
----
|
|
model = modelLoad(name)
|
|
----
|
|
|
|
Reads a `.glb` through the same lookup as every other asset (game directory, data directory, packed database), uploads its meshes and materials to the GPU once, and keeps its node tree, skins and animations for instancing. Call it once at startup and instance the handle as many times as needed; loading the same file twice makes two copies on the GPU. It needs the 3D scene, so on a machine without a suitable GPU it fails like any other 3D call. A bad file ends the script with a message naming the problem: the file could not be read, is not glTF, refers to an external buffer or image, fails validation, holds a primitive that could not be uploaded, has a skin with more than `128` joints, or has a skinned mesh naming a joint numbered `128` or more. A skinned mesh naming a joint beyond the skins that use it loads, as does an animation whose keyframes cannot be read (that sampler is skipped); either is noted in the program trace. A texture that cannot be decoded, or a missing KTX2 image, does not fail the load; the material goes without that map and the program trace (`--program`) says which one.
|
|
|
|
*Returns:* The model handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<modelinstance,modelInstance>>, <<modelgetanimations,modelGetAnimations>>, <<modeldelete,modelDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Load once, place three times.
|
|
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)
|
|
----
|
|
|
|
Frees the model's meshes, materials and parsed data. Every instance made from it is forgotten at the same time: its nodes stay in the scene (position, parent and children intact) but draw nothing, its animations stop where they are, and a later `animation*` call on its root raises an error because the node is no longer a model instance. Delete the instance roots with `nodeDelete` when the nodes are not wanted either. A handle that is not a loaded model raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<modelload,modelLoad>>, <<modelinstance,modelInstance>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Leaving the hangar level: drop its props and the model behind them.
|
|
function unloadHangar()
|
|
for _, crate in ipairs(hangarCrates) do
|
|
nodeDelete(crate)
|
|
end
|
|
hangarCrates = {}
|
|
modelDelete(crateModel)
|
|
crateModel = nil
|
|
end
|
|
----
|
|
|
|
[#modelgetanimations]
|
|
==== modelGetAnimations
|
|
|
|
[source,text]
|
|
----
|
|
names = modelGetAnimations(model)
|
|
----
|
|
|
|
Lists the model's animation clips in file order as a table of strings, so `names[3]` is the clip that `animationPlay(node, 3)` plays. A clip the exporter left unnamed gives an empty string; it can still be played by number. A model with no animations gives an empty table. A handle that is not a loaded model raises an error.
|
|
|
|
*Returns:* A table of strings indexed from `1`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<animationplay,animationPlay>>, <<modelload,modelLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Find the clips by name once, so the rest of the game uses numbers.
|
|
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)
|
|
----
|
|
|
|
Builds the model's node tree under a new root node, itself under `parent` or the scene root. The root is a plain node: position, rotate, scale or parent it like any other, and delete it with `nodeDelete` to remove the whole instance. The model's own nodes take their glTF names (or their mesh's name when the node is unnamed) so `nodeFind(name, node)` reaches them; a mesh with several materials becomes one child node per part; lights in the file become scene lights with the file's color, intensity, range and cone; skins are attached and morph weights set. The nodes stand in the file's pose until an animation plays. A handle that is not a loaded model or a `parent` that is not a node raises an error, as does running out of nodes.
|
|
|
|
*Returns:* The instance's root node handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<modelload,modelLoad>>, <<nodefind,nodeFind>>, <<animationplay,animationPlay>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A turret on the tank's hull, so it turns with the hull and pivots on its own.
|
|
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]
|
|
=== Mouse
|
|
|
|
Singe reads up to four mice, and treats lightguns as mice. In the default single-mouse mode every pointing device drives one cursor reported as mouse `0`; `mouseSetMode(MOUSE_MANY)` tells the devices apart, which a two-player lightgun cabinet needs. Positions arrive in overlay coordinates, not screen pixels, through `onMouseMoved` (see <<enginecallbacks,Engine Callbacks>>) and on demand from `mouseGetPosition`. Mouse buttons and the wheel are switches like keys: they reach `onInputPressed` and `onInputReleased` when `controls.cfg` maps a code from the `MOUSE_0` through `MOUSE_3` tables, and the shipped defaults map the left button to `SWITCH_BUTTON3`, the right to `SWITCH_BUTTON1` and the middle to `SWITCH_BUTTON2`. The engine starts with the mouse captured, and enabled unless `--nomouse` was given or no mouse was found.
|
|
|
|
[#mousedisable]
|
|
==== mouseDisable
|
|
|
|
[source,text]
|
|
----
|
|
mouseDisable()
|
|
----
|
|
|
|
Turns mouse input off, exactly as `mouseSetEnabled(false)` does. It is a function defined in `Framework.singe`, kept so older games run unchanged.
|
|
|
|
*Notes:* Legacy alias.
|
|
|
|
WARNING: New code should call <<mousesetenabled,mouseSetEnabled>> with `false`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousesetenabled,mouseSetEnabled>>, <<mouseenable,mouseEnable>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Older game code; the modern spelling is mouseSetEnabled(false).
|
|
function cutsceneBegin()
|
|
mouseDisable()
|
|
discSearch(CUTSCENE_START)
|
|
end
|
|
----
|
|
|
|
[#mouseenable]
|
|
==== mouseEnable
|
|
|
|
[source,text]
|
|
----
|
|
mouseEnable()
|
|
----
|
|
|
|
Turns mouse input on, exactly as `mouseSetEnabled(true)` does, and like it stays off when Singe was started with `--nomouse`. It is a function defined in `Framework.singe`, kept so older games run unchanged.
|
|
|
|
*Notes:* Legacy alias.
|
|
|
|
WARNING: New code should call <<mousesetenabled,mouseSetEnabled>> with `true`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousesetenabled,mouseSetEnabled>>, <<mousedisable,mouseDisable>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Older game code; the modern spelling is mouseSetEnabled(true).
|
|
function cutsceneEnd()
|
|
mouseEnable()
|
|
discSearch(LEVEL_START)
|
|
end
|
|
----
|
|
|
|
[#mousegetname]
|
|
==== mouseGetName
|
|
|
|
[source,text]
|
|
----
|
|
name = mouseGetName(mouse)
|
|
----
|
|
|
|
The name the mouse driver gives a device, or `nil` when there is no device with that index. Two guns on a cabinet are told apart by it on a service screen, which is otherwise guesswork when both report as mouse `0` and `1`. An index outside `0` to `3` aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `mouse` -- device index, `0` through `3`.
|
|
|
|
*Returns:* string, or `nil`.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<mousehowmany,mouseHowMany>>, <<mousesetmode,mouseSetMode>>, <<controllergetname,controllerGetName>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Name the guns on the calibration screen.
|
|
mouseSetMode(MOUSE_MANY)
|
|
for device = 0, mouseHowMany() - 1 do
|
|
fontPrint(20, 20 + device * 18, string.format("Gun %d: %s", device + 1, mouseGetName(device)))
|
|
end
|
|
----
|
|
|
|
[#mousegetposition]
|
|
==== mouseGetPadding
|
|
|
|
[source,text]
|
|
----
|
|
padding = mouseGetPadding()
|
|
----
|
|
|
|
Hypseus extension, always `0`, for the reason <<controllergetpadding,`controllerGetPadding`>> gives. Hypseus adds this offset to the input codes its mice report in relative mode; Singe's codes carry no such offset, so there is nothing to subtract.
|
|
|
|
*Returns:* an integer, always `0`.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<controllergetpadding,controllerGetPadding>>
|
|
|
|
==== mouseGetPosition
|
|
|
|
[source,text]
|
|
----
|
|
x, y = mouseGetPosition(index)
|
|
----
|
|
|
|
Returns the last reported position of a mouse in overlay coordinates, the same values the most recent `onMouseMoved` for that device carried. In single-mouse mode only mouse `0` moves; in many-mouse mode each device has its own position, starting at the center of the video. The position stops updating while mouse input is disabled. Poll it from `onOverlayUpdate` or `singeMain` when that is simpler than tracking the callback. An index outside `0` to `3` aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `index` -- mouse index, `0` through `3`.
|
|
|
|
*Returns:* two integers, `x` and `y`, in overlay coordinates.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<singewantscrosshairs,singeWantsCrosshairs>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw the crosshair where the gun points.
|
|
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()
|
|
----
|
|
|
|
Returns the number of mice the engine found at startup, `0` through `4`. Devices plugged in later are not counted. When none were found the mouse stays disabled. Use it to decide whether to offer a second lightgun player.
|
|
|
|
*Returns:* integer, `0` to `4`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousesetmode,mouseSetMode>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Two guns present: give each player a cursor.
|
|
if mouseHowMany() >= 2 then
|
|
mouseSetMode(MOUSE_MANY)
|
|
players = 2
|
|
else
|
|
players = 1
|
|
end
|
|
----
|
|
|
|
[#mousesetcaptured]
|
|
==== mouseSetCaptured
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetCaptured(captured)
|
|
----
|
|
|
|
Grabs the mouse, confining it to the Singe window and hiding the cursor, or releases it and shows the cursor again. The engine captures the mouse at startup, so most games never call this; a windowed level editor or a game that wants the desktop cursor visible releases it. The switch mapped to `INPUT_GRAB` in `controls.cfg` toggles the same state from the keyboard.
|
|
|
|
*Parameters:*
|
|
|
|
* `captured` -- boolean; `true` grabs and hides, `false` releases and shows.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<mousesetenabled,mouseSetEnabled>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The level editor wants the desktop cursor; gameplay hides it again.
|
|
function editorToggle()
|
|
editing = not editing
|
|
mouseSetCaptured(not editing)
|
|
end
|
|
----
|
|
|
|
[#mousesetenabled]
|
|
==== mouseSetEnabled
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetEnabled(enabled)
|
|
----
|
|
|
|
Turns mouse input on or off. While off, motion, button and wheel events are discarded before they reach the script: `onMouseMoved` stops firing, mouse buttons no longer produce switches, and `mouseGetPosition` keeps returning the last position it had. The cursor itself still moves. Enabling honors `--nomouse`; when Singe was started with that option the mouse stays off. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<mousesetcaptured,mouseSetCaptured>>, <<mousesetmode,mouseSetMode>>, <<mouseenable,mouseEnable>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Ignore the gun while the attract loop plays.
|
|
function attractBegin()
|
|
mouseSetEnabled(false)
|
|
discSearch(ATTRACT_START)
|
|
end
|
|
|
|
function attractEnd()
|
|
mouseSetEnabled(true)
|
|
end
|
|
----
|
|
|
|
[#mousesetmode]
|
|
==== mouseSetMode
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetMode(mode)
|
|
----
|
|
|
|
Chooses how mice are read. In `MOUSE_SINGLE` (the default) the window's pointer events drive one cursor reported as device `0`, with absolute positions and relative motion both converted to overlay coordinates. In `MOUSE_MANY` each physical device is read separately: relative motion is integrated into a per-device position clamped to the video area, absolute devices such as tablets and some lightguns are mapped onto the video, and `onMouseMoved` reports the device index. Both modes deliver the same five buttons and the vertical wheel as switches. `SINGLE_MOUSE` and `MANY_MOUSE` are accepted as older spellings; any other value aborts the script. `--manymouse` is the user's own decision and outranks this call: with it given the mode is `MOUSE_MANY` from the start and `mouseSetMode(MOUSE_SINGLE)` does not switch it off, so a two gun cabinet can be made to work from outside a game that never asks.
|
|
|
|
*Parameters:*
|
|
|
|
* `mode` -- `MOUSE_SINGLE` (`100`) or `MOUSE_MANY` (`200`).
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Cooperative lightgun game: each gun steers its own 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]
|
|
=== Music
|
|
|
|
Background music, loaded and played apart from the sound effects and carrying its own volume. Hypseus runs music on a second mixer for exactly that reason, and Singe gives it its own gain instead: <<musicsetvolume,`musicSetVolume`>> never moves a sound effect and <<soundsetvolume,`soundSetVolume`>> never moves the music.
|
|
|
|
A music handle is its own kind of handle, not a sound handle and not a channel: one handle is one track, and it plays, pauses, resumes and stops on its own. There is no channel limit, unlike the sixteen sound effect channels.
|
|
|
|
The volume scale is Hypseus's, `0` to `128`, twice the `0` to `63` the `sound` family uses, so a game written for Hypseus keeps its own numbers. Music starts as loud as the sound effects do, at whatever `--volume_nonvldp` set; Hypseus instead starts its music mixer a third of the way up, which every Hypseus game immediately overwrites with `musicSetVolume(soundGetVolume() * 2)`.
|
|
|
|
Every format the sound family reads is read here: WAV, Ogg Vorbis, Opus, FLAC, MP3, WavPack and the rest. The trace header written by `--program` and `--trace` lists exactly which decoders the build carries, on its `Audio:` line.
|
|
|
|
Pausing the engine (the pause key, or `singeSetPauseFlag`) pauses the music with it, and resuming brings it back; a reload stops and frees every track.
|
|
|
|
[#musicisplaying]
|
|
==== musicIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = musicIsPlaying([id])
|
|
----
|
|
|
|
Hypseus extension. Whether that track is playing, or -- with no handle -- whether any music is. A paused track is not playing.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- optional number; a music handle.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
if not musicIsPlaying(theme) then
|
|
musicPlay(theme, -1)
|
|
end
|
|
----
|
|
|
|
[#musicload]
|
|
==== musicLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = musicLoad(filename)
|
|
----
|
|
|
|
Hypseus extension. Loads a music track through the virtual file system and returns its handle. The whole file is decoded up front, as a sound is, so a long track costs memory; the handle is valid until <<musicunload,`musicUnload`>> or a reload.
|
|
|
|
A file that is missing, or that no compiled-in decoder can read, terminates the script, as <<soundload,`soundLoad`>> does.
|
|
|
|
*Parameters:*
|
|
|
|
* `filename` -- string; a path the virtual file system can reach.
|
|
|
|
*Returns:* number, the music handle.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicplay,musicPlay>>, <<musicunload,musicUnload>>, <<soundload,soundLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("music/attract.mp3")
|
|
musicSetVolume(96)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
[#musicpause]
|
|
==== musicPause
|
|
|
|
[source,text]
|
|
----
|
|
musicPause([id])
|
|
----
|
|
|
|
Hypseus extension. Pauses that track where it is, or every piece of music when no handle is given. <<musicresume,`musicResume`>> picks it up again.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- optional number; a music handle.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicresume,musicResume>>, <<musicstop,musicStop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The player opened the map; hold the music rather than restart it.
|
|
musicPause()
|
|
----
|
|
|
|
[#musicplay]
|
|
==== musicPlay
|
|
|
|
[source,text]
|
|
----
|
|
musicPlay(id [, loops])
|
|
----
|
|
|
|
Hypseus extension. Plays a track from its beginning. `loops` means what <<soundplay,`soundPlay`>>'s does: `0`, the default, plays once, `-1` plays forever, and `N` repeats `N` more times. The count is clamped to `-1` to `64`, as Hypseus clamps it, so a game asking for more gets what it would have got there.
|
|
|
|
Playing a track that is already playing starts it again from the beginning.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; a music handle.
|
|
* `loops` -- optional number; `-1` to `64`. Default `0`.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
musicPlay(theme, -1) -- Round and round until something stops it.
|
|
----
|
|
|
|
[#musicresume]
|
|
==== musicResume
|
|
|
|
[source,text]
|
|
----
|
|
musicResume([id])
|
|
----
|
|
|
|
Hypseus extension. Resumes a paused track, or every paused piece of music when no handle is given.
|
|
|
|
Hypseus's own zero argument form does nothing at all; Singe's resumes everything, which is what pairs with <<musicpause,`musicPause`>>.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- optional number; a music handle.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicpause,musicPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
musicResume()
|
|
----
|
|
|
|
[#musicsetvolume]
|
|
==== musicSetVolume
|
|
|
|
[source,text]
|
|
----
|
|
volume = musicSetVolume(volume)
|
|
----
|
|
|
|
Hypseus extension. Sets the gain over every piece of music at once, on Hypseus's `0` to `128` scale, and answers the level it took. It moves nothing but music: the sound effects keep whatever <<soundsetvolume,`soundSetVolume`>> gave them, and `soundSetVolume` leaves the music alone.
|
|
|
|
A level outside `0` to `128` terminates the script. The level applies to tracks already playing and to any loaded afterwards, and goes back to the starting level on a reload.
|
|
|
|
*Parameters:*
|
|
|
|
* `volume` -- number; `0` to `128`.
|
|
|
|
*Returns:* number, the level that was set.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<soundsetvolume,soundSetVolume>>, <<musicplay,musicPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- What a Hypseus game writes: the sound level, doubled onto the music scale.
|
|
musicSetVolume(soundGetVolume() * 2)
|
|
----
|
|
|
|
[#musicstop]
|
|
==== musicStop
|
|
|
|
[source,text]
|
|
----
|
|
musicStop([id [, fadeMs]])
|
|
----
|
|
|
|
Hypseus extension. Stops that track, or every piece of music when no handle is given. `fadeMs` fades it out over that many milliseconds instead of cutting it; `0`, the default, stops at once. A negative fade terminates the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- optional number; a music handle.
|
|
* `fadeMs` -- optional number; milliseconds to fade out over. Default `0`.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
musicStop(theme, 1500) -- Away over a second and a half.
|
|
----
|
|
|
|
[#musicunload]
|
|
==== musicUnload
|
|
|
|
[source,text]
|
|
----
|
|
musicUnload(id)
|
|
----
|
|
|
|
Hypseus extension. Stops the track and frees it. The handle is invalid afterwards and using it terminates the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; a music handle.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<musicload,musicLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
musicUnload(theme)
|
|
----
|
|
|
|
[#nav]
|
|
=== Nav
|
|
|
|
Navigation meshes baked from scene geometry, and the crowd agents that walk them (see <<scenes3d,3D Scenes>>, Navigation). `navNew` and `navLoad` return a mesh handle and `navAgentNew` an agent handle; both are integers, checked on every call, and an invalid one terminates the script. Positions are world units, speeds units a second and slopes degrees. Up to eight meshes at once, 128 agents on each and 256 in all. The queries and the agents need a mesh that `navBuild` has baked or `navLoad` has read; before that the queries return `nil`, `navDraw` draws nothing and `navAgentNew` raises an error.
|
|
|
|
[#navaddnode]
|
|
==== navAddNode
|
|
|
|
[source,text]
|
|
----
|
|
navAddNode(nav, node)
|
|
----
|
|
|
|
Adds the node's mesh, and the mesh of every node under it, to the geometry the navigation mesh will be baked from, as they stand in the world right now, so place them first. A model instance's root node adds the whole model, and a level built from primitives can be added node by node. Only mesh geometry counts: bodies, sprites and text add nothing. Raises an error once the mesh has been built.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navnew,navNew>>, <<navbuild,navBuild>>, <<modelinstance,modelInstance>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The ground and the walls are walkable geometry; the crates are not, they move.
|
|
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)
|
|
----
|
|
|
|
Takes the agent off the mesh and frees its handle; the node stays where it is and is not moved again. Deleting the mesh with `navDelete` does this for every agent on it. An invalid handle terminates the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentnew,navAgentNew>>, <<navdelete,navDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A guard that dies stops navigating; the ragdoll takes the model from here.
|
|
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)
|
|
----
|
|
|
|
How the crowd is moving the agent this frame, in world units a second along each axis; its length runs up to the agent's speed and falls to zero as it stops. For choosing between an idle, a walk and a run animation, or scaling the walk cycle to the ground covered.
|
|
|
|
*Returns:* Three numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentmoveto,navAgentMoveTo>>, <<animationplay,animationPlay>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Whether the agent has reached the last target `navAgentMoveTo` gave it: within one and a half of its radii of the target and nearly stopped. It turns `true` at the moment `onNavArrived` fires and stays `true` until the next `navAgentMoveTo`; it is `false` for an agent that was never sent anywhere or was stopped on the way.
|
|
|
|
*Returns:* True or false.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<onnavarrived,onNavArrived>>, <<navagentmoveto,navAgentMoveTo>>, <<navagentstop,navAgentStop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The patrol waits at each post for a while before moving on.
|
|
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)
|
|
----
|
|
|
|
Sends the agent walking to the point of the mesh nearest `x, y, z`, looking up to 2 units sideways and 4 up or down for one. The crowd plans the route over the following frames and steers the agent round the other agents on the way; `onNavArrived` fires once it gets there. Returns `false`, and leaves the agent as it was, when no point of the mesh is near enough or the crowd refuses the request. A new target replaces the old one at any time.
|
|
|
|
*Parameters:*
|
|
|
|
* `agent` -- the handle from `navAgentNew`.
|
|
* `x`, `y`, `z` -- the destination in world units; it is snapped onto the mesh.
|
|
|
|
*Returns:* True when the agent set off.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentstop,navAgentStop>>, <<navnearest,navNearest>>, <<navpath,navPath>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The sidekick follows the hero, re-aimed twice a second.
|
|
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) -- The hero is off the mesh: jumping, swimming
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#navagentnew]
|
|
==== navAgentNew
|
|
|
|
[source,text]
|
|
----
|
|
agent = navAgentNew(nav, node, radius, height, speed)
|
|
----
|
|
|
|
Puts an agent of that size on the mesh at the point nearest the node's world position (the raw position when nothing is within 2 units sideways and 4 up or down), walking at `speed` units a second and accelerating at 8 units a second squared. The radius and height are kept at `0.01` or more, as `navNew` keeps its own. From then on the engine moves the node along the mesh every frame, in its parent's frame, and turns it about Y to face the way it is going whenever it moves faster than `0.05` units a second; nodes look down -Z, as models do. The crowd steers agents round each other, anticipates turns and keeps them apart. To drive a player controller on the node instead of placing the node, see `navAgentSetPlayer`. Raises an error when the mesh is not built or 128 agents are already on it.
|
|
|
|
*Parameters:*
|
|
|
|
* `nav` -- a built mesh from `navNew` or `navLoad`.
|
|
* `node` -- the node to move; usually a model instance's root.
|
|
* `radius` -- the agent's radius for keeping clear of others, in world units; the mesh's own radius or less.
|
|
* `height` -- the agent's height, in world units.
|
|
* `speed` -- top walking speed, in world units a second.
|
|
|
|
*Returns:* The agent's handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentmoveto,navAgentMoveTo>>, <<navagentsetplayer,navAgentSetPlayer>>, <<navagentdelete,navAgentDelete>>, <<navrandompoint,navRandomPoint>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
With `true`, the agent stops placing its node and instead feeds its velocity, each frame, to the player controller on the node as a `playerMove` (horizontal only), so stairs, slopes, pushing and collisions keep working; before every crowd update the controller's world position is read back as the agent's own, so being shoved off course is planned around. The node still turns to face its way. The node needs a controller from `playerNew`, or nothing moves. With `false` the agent goes back to placing the node itself.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playernew,playerNew>>, <<playermove,playerMove>>, <<navagentnew,navAgentNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A companion who follows the hero through the level and can be knocked about.
|
|
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)
|
|
----
|
|
|
|
Forgets the agent's target: the crowd stops steering it toward anything, it slows to a stand where it is, and `onNavArrived` does not fire. Other agents still walk round it. Send it somewhere with `navAgentMoveTo` to set off again.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Guards freeze when the alarm sounds and turn toward the noise.
|
|
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)
|
|
----
|
|
|
|
Bakes the walkable mesh from everything `navAddNode` added: floors and slopes up to the mesh's angle, steps up to its height, with the agent's radius trimmed off round every wall and drop, and anything with less than the agent's height above it left out. The bake works on cells a third of the agent's radius across and covers at most 4096 by 4096 of them, so a small radius over a big level is slow and can fail; a tenth of a second for Sponza on a desktop, a few seconds on a Raspberry Pi. The geometry that was added is freed afterward, and nothing more can be added; bake another mesh instead. `navSave` keeps the result. Raises an error when nothing was added, the mesh is already built, or the bake fails.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navnew,navNew>>, <<navaddnode,navAddNode>>, <<navsave,navSave>>, <<navdraw,navDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The bake happens once, while the title screen is up, and is kept for next time.
|
|
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) -- One frame, to eyeball the bake
|
|
end
|
|
----
|
|
|
|
[#navdelete]
|
|
==== navDelete
|
|
|
|
[source,text]
|
|
----
|
|
navDelete(nav)
|
|
----
|
|
|
|
Frees the mesh and every agent on it; their nodes stay where they are. The mesh handle and the agent handles are invalid afterward and terminate the script if used. The slot is free for a new `navNew` or `navLoad`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navnew,navNew>>, <<navagentdelete,navAgentDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function loadLevel(index)
|
|
if levelNav then
|
|
navDelete(levelNav) -- Takes the old level's agents with it
|
|
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)
|
|
----
|
|
|
|
Draws the baked mesh's polygons as lines over the scene for this frame, cyan unless colored, each polygon as a fan of triangles so a polygon with more than three sides shows its diagonals. Call it every frame while looking; it costs three lines per triangle, and a mesh that is not built yet draws nothing. For checking that a bake covers the floor and stays off the furniture, alongside `physicsSetDebug`.
|
|
|
|
*Parameters:*
|
|
|
|
* `nav` -- the mesh.
|
|
* `r`, `g`, `b` -- the line color, `0` to `255` each; default `0, 255, 255`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<linedraw,lineDraw>>, <<navbuild,navBuild>>, <<physicssetdebug,physicsSetDebug>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if showNavMesh then
|
|
navDraw(nav) -- Cyan over the floor
|
|
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)
|
|
----
|
|
|
|
Loads a mesh `navSave` wrote, looked for among the game's files first (so a baked mesh can ship with the game, packed or not) and then in the game's data folder. The mesh comes back built, ready for agents and queries, and takes no more `navAddNode`. The slope and step it was baked with are in the file; the agent radius and height are what the crowd steers with, so give the values it was baked for. Raises an error when the file cannot be read, is not a navigation mesh, is shorter than the mesh its header describes (truncated, or edited by hand), or no mesh slot is free.
|
|
|
|
*Parameters:*
|
|
|
|
* `name` -- the file's name, relative to the game directory or the data folder.
|
|
* `agentRadius` -- the agent radius the mesh was baked for, in world units.
|
|
* `agentHeight` -- the agent height it was baked for, in world units.
|
|
|
|
*Returns:* The mesh handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navsave,navSave>>, <<navnew,navNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The shipped bake for the museum; agents 0.4 wide and 1.7 tall.
|
|
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)
|
|
----
|
|
|
|
The closest point of the mesh to a point, searching up to 2 units sideways and 4 up or down, or `nil` when there is nothing that near. For snapping a spawn point, a drop or a click onto the floor before handing it to an agent, and for finding the floor's height under a point.
|
|
|
|
*Parameters:*
|
|
|
|
* `nav` -- the mesh.
|
|
* `x`, `y`, `z` -- the point to snap, in world units.
|
|
|
|
*Returns:* Three numbers, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navpath,navPath>>, <<navagentmoveto,navAgentMoveTo>>, <<sceneunproject,sceneUnproject>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Spawn the enemy on the floor nearest the spawner, or not at all.
|
|
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)
|
|
----
|
|
|
|
Starts a navigation mesh for agents of that radius and height that can climb steps up to `maxStep` units and slopes up to `maxSlope` degrees. The radius and height are kept at `0.01` or more, the slope between `0` and `89` and the step at `0` or more. The radius also sets the bake's resolution, a cell being a third of it, so it decides how long `navBuild` takes and how big a level it can cover. Add geometry with `navAddNode`, then `navBuild`. Raises an error when eight meshes already exist.
|
|
|
|
*Parameters:*
|
|
|
|
* `agentRadius` -- how wide the agents are, in world units; walls are padded by this.
|
|
* `agentHeight` -- how tall they are; lower ceilings are not walkable.
|
|
* `maxSlope` -- the steepest walkable slope, in degrees.
|
|
* `maxStep` -- the highest step they climb, in world units.
|
|
|
|
*Returns:* The mesh handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navaddnode,navAddNode>>, <<navbuild,navBuild>>, <<navload,navLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Sponza for an agent 0.4 wide and 1.6 tall that climbs 0.3 and walks slopes to 45 degrees.
|
|
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)
|
|
----
|
|
|
|
The corners of the shortest walk from one point to another, both snapped onto the mesh first, as a table of `{ x, y, z }` tables with the first at the start and the last at the end; up to 256 corners. When the end cannot be reached the walk stops at the nearest reachable point on the way, so the last corner is always somewhere an agent can stand. `nil` when either end is nowhere near the mesh or no path is found. For drawing a route, measuring how far something is on foot, or moving something along it yourself; agents do this on their own.
|
|
|
|
*Parameters:*
|
|
|
|
* `nav` -- the mesh.
|
|
* `x0`, `y0`, `z0` -- where the walk starts.
|
|
* `x1`, `y1`, `z1` -- where it ends.
|
|
|
|
*Returns:* A table of `{ x, y, z }` tables, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentmoveto,navAgentMoveTo>>, <<navraycast,navRaycast>>, <<navnearest,navNearest>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- How far the exit is on foot, for the map's distance readout.
|
|
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)
|
|
----
|
|
|
|
A random point on the walkable mesh, anywhere it reaches, for wandering, scattering pickups or placing a spawn. `nil` only when the mesh is not built.
|
|
|
|
*Returns:* Three numbers, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentmoveto,navAgentMoveTo>>, <<navnearest,navNearest>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Scatter twenty coins over the floor, each a little above it.
|
|
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)
|
|
----
|
|
|
|
Whether a straight walk from one point to another leaves the mesh, at a wall or a drop, and where it would. The start is snapped onto the mesh; `false` alone means the way is clear, or that the start is nowhere near the mesh. When blocked, the point is where the line crosses the mesh's edge, its height interpolated between the two ends. A cheap check for whether a shortcut exists before asking `navPath`.
|
|
|
|
*Parameters:*
|
|
|
|
* `nav` -- the mesh.
|
|
* `x0`, `y0`, `z0` -- where the walk starts.
|
|
* `x1`, `y1`, `z1` -- where it would end.
|
|
|
|
*Returns:* `false`, or `true` and the point where the walk leaves the mesh.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navpath,navPath>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A thrown grenade only rolls as far as the floor lets it.
|
|
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)
|
|
----
|
|
|
|
Writes the baked mesh to the game's data folder under `name`, for `navLoad` next time, or to copy into the game directory to ship. The mesh must be built. Raises an error when it is not, or when the file cannot be written.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navload,navLoad>>, <<navbuild,navBuild>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Baked once on this machine, then loaded from the data folder in later runs.
|
|
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.")
|
|
----
|
|
|
|
[#node]
|
|
=== Node
|
|
|
|
Every object in a scene is a node in one tree: node `0` is the root, and
|
|
every other node hangs under a parent. A node carries a position, a
|
|
rotation, and a scale relative to that parent, and may draw a mesh with a
|
|
material, show a sprite or a line of text, or shine as a light; children
|
|
inherit the transform of everything above them. Handles are integers
|
|
returned by `nodeNew`, `modelInstance`, and `lightNew`; a handle whose node
|
|
has been deleted aborts the script when passed back, and a freed handle may
|
|
be reused by a later `nodeNew`. Positions and sizes are in scene units and
|
|
rotations in degrees, with +Y up and -Z forward as described in
|
|
<<scenes3d,3D Scenes>>.
|
|
|
|
[#nodenew]
|
|
==== nodeNew
|
|
|
|
[source,text]
|
|
----
|
|
node = nodeNew()
|
|
node = nodeNew(parent)
|
|
----
|
|
|
|
Creates an empty node at its parent's origin, unrotated, at scale `1`,
|
|
visible, casting shadows, with no mesh or material. Without `parent` the
|
|
node goes under the root; with one it becomes that node's last child and
|
|
moves with it from then on. Empty nodes serve as pivots and groups: place
|
|
one, then hang meshes, lights, or a camera under it and move the group as a
|
|
whole.
|
|
|
|
*Returns:* The node handle.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmesh,nodeSetMesh>>, <<nodesetparent,nodeSetParent>>, <<nodedelete,nodeDelete>>, <<lightnew,lightNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A turret whose barrel pivots on the turret, which turns on the base.
|
|
local steel = materialNew()
|
|
materialSetColor(steel, 140, 145, 150)
|
|
local base = nodeNew()
|
|
local turret = nodeNew(base)
|
|
local barrel = nodeNew(turret)
|
|
nodeSetMesh(base, meshCylinder(1, 0.3), steel)
|
|
nodeSetMesh(turret, meshSphere(0.6), steel)
|
|
nodeSetMesh(barrel, meshCylinder(0.1, 1.2), steel)
|
|
nodeSetPosition(barrel, 0, 0, -0.8)
|
|
nodeSetRotation(barrel, 90, 0, 0)
|
|
----
|
|
|
|
[#nodedelete]
|
|
==== nodeDelete
|
|
|
|
[source,text]
|
|
----
|
|
nodeDelete(node)
|
|
----
|
|
|
|
Removes the node and everything under it, freeing every handle in that
|
|
subtree; a sprite or text on any of them is released too. Meshes and
|
|
materials the nodes drew are untouched, since other nodes may share them.
|
|
If the deleted node was the camera, the scene falls back to the default
|
|
view. The root node cannot be deleted; asking aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodenew,nodeNew>>, <<nodesetvisible,nodeSetVisible>>, <<meshdelete,meshDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Shots live for two seconds, then their nodes go.
|
|
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)
|
|
----
|
|
|
|
Searches depth first for the first node named `name`, starting at `root`
|
|
(the scene root when omitted) and checking `root` itself before its
|
|
children, each child's subtree in turn. Returns `nil` when nothing
|
|
matches. Model nodes carry the names from their glTF file, so a bone,
|
|
a mesh, or a marker empty placed in Blender can be found by that name
|
|
under the instance's root. Names set with `nodeSetName` are found the same
|
|
way.
|
|
|
|
*Returns:* A node handle, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetname,nodeSetName>>, <<nodegetname,nodeGetName>>, <<modelinstance,modelInstance>>, <<nodegetchildren,nodeGetChildren>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hang a torch on the hero's right hand, found by its bone name.
|
|
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)
|
|
----
|
|
|
|
The node's direct children as a table of handles, in the order they were
|
|
attached; re-parenting a node puts it at the end of its new parent's list.
|
|
Grandchildren are not included; recurse to walk a whole subtree. An
|
|
empty table means the node has no children.
|
|
|
|
*Returns:* A table of node handles.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodegetparent,nodeGetParent>>, <<nodefind,nodeFind>>, <<nodesetparent,nodeSetParent>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Give every part of a loaded prop the same worn material.
|
|
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)
|
|
----
|
|
|
|
The current weight of one morph target of the node's mesh, given by name or
|
|
by number from `1` in the order `nodeGetMorphs` lists them. The node is the
|
|
one carrying the mesh, not a model instance's root. A name or number the
|
|
mesh does not have aborts the script.
|
|
|
|
*Returns:* A number, `0` until something sets the target.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorphs,nodeGetMorphs>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Ease the smile in over several frames instead of snapping it on.
|
|
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)
|
|
----
|
|
|
|
The morph targets of the node's mesh as a table of names, in the order the
|
|
file stores them; that order is the numbering `nodeSetMorph` and
|
|
`nodeGetMorph` accept. Exporters that write no names give empty strings,
|
|
and those targets can still be set by number. A node with no mesh, or a
|
|
mesh without targets, gives an empty table.
|
|
|
|
*Returns:* A table of strings.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorph,nodeGetMorph>>, <<modelload,modelLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Map the face's target names to numbers once, so lip sync can index them.
|
|
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)
|
|
----
|
|
|
|
The node's name: whatever `nodeSetName` gave it or its glTF name for a
|
|
model node, an empty string when it has neither. The root node is named
|
|
`root`.
|
|
|
|
*Returns:* A string.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetname,nodeSetName>>, <<nodefind,nodeFind>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Print a model's node tree to the console while working out bone names.
|
|
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)
|
|
----
|
|
|
|
The handle of the node's parent, or `nil` for the root. Every other node
|
|
has a parent, even if only the root.
|
|
|
|
*Returns:* A node handle, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetparent,nodeSetParent>>, <<nodegetchildren,nodeGetChildren>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Climb from a picked mesh node to the top of its model instance.
|
|
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)
|
|
----
|
|
|
|
The node's position relative to its parent, as last set by
|
|
`nodeSetPosition`, `nodeMove`, or the engine (a body, a vehicle, or an
|
|
animation moving it). For where the node is in the world, use
|
|
`nodeGetWorldPosition`.
|
|
|
|
*Returns:* Three numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetposition,nodeSetPosition>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<nodemove,nodeMove>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The camera trails the car, three units up and nine behind.
|
|
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)
|
|
----
|
|
|
|
The node's rotation relative to its parent as a unit quaternion, the form
|
|
models and animations store. Use it to copy a rotation between nodes or
|
|
to keep a pose without the ambiguity of Euler angles.
|
|
|
|
*Returns:* Four numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetquaternion,nodeSetQuaternion>>, <<nodegetrotation,nodeGetRotation>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Remember the door's pose before the ragdoll test, then put it back.
|
|
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)
|
|
----
|
|
|
|
The node's rotation relative to its parent as Euler angles in degrees,
|
|
decomposed the same way `nodeSetRotation` composes them: turned about Y,
|
|
then tilted about X, then rolled about Z. The angles describe the same
|
|
rotation that was set but need not be the same numbers: `X` comes back
|
|
between `-90` and `90`, and when it reaches `90` or `-90` the twist is
|
|
given to `Y` with `Z` reported as `0`.
|
|
|
|
*Returns:* Three numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A compass in the overlay from the ship's heading.
|
|
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)
|
|
----
|
|
|
|
The node's own scale per axis, as set by `nodeSetScale` (`1, 1, 1` for a
|
|
new node). Parents' scales are not folded in.
|
|
|
|
*Returns:* Three numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetscale,nodeSetScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Grow the power-up toward full size, a little each frame.
|
|
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)
|
|
----
|
|
|
|
Where the node's origin is in world space, with every parent's transform
|
|
applied, as of the last rendered frame. Changes made this frame (yours, or
|
|
a body's) show after the next render, and a node created this frame reports
|
|
`0, 0, 0` until then. This is the position to feed `sceneProject` or a
|
|
distance check.
|
|
|
|
*Returns:* Three numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneproject,sceneProject>>, <<nodelookat,nodeLookAt>>, <<nodegetposition,nodeGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A name tag in the overlay above the duck, wherever the physics took it.
|
|
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)
|
|
----
|
|
|
|
Rotates the node so its own -Z points at a world position, keeping its +Y
|
|
as close to world up as the direction allows. That is what cameras and
|
|
lights want, and it serves a turret or a head tracking a target as well.
|
|
Only the node's rotation changes; its position stays. If the target
|
|
coincides with the node's position nothing changes.
|
|
|
|
*Notes:* The world point is converted into the parent's space using the
|
|
parent's transform from the last rendered frame; a node whose parent moved
|
|
this same frame aims from where the parent was.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<cameraset,cameraSet>>, <<lightnew,lightNew>>, <<nodesetrotation,nodeSetRotation>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Moves the node along its own axes: the offset is turned by the node's
|
|
current rotation and added to its position, so `nodeMove(ship, 0, 0, -0.1)`
|
|
drives the ship forward wherever it is pointing and `nodeMove(ship, 0.1, 0,
|
|
0)` strafes it right. Use `nodeSetPosition` for an absolute place in the
|
|
parent's space.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetposition,nodeSetPosition>>, <<noderotate,nodeRotate>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Tank controls: turn with left and right, drive with up and down.
|
|
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)
|
|
----
|
|
|
|
Turns the node by the given degrees about its own axes, on top of its
|
|
current rotation: `nodeRotate(wheel, 5, 0, 0)` every frame spins the wheel
|
|
on its own X however the axle is tilted. The three angles are combined the
|
|
way `nodeSetRotation` combines them (Y, then X, then Z) before being
|
|
applied. Positive angles turn counter-clockwise when looking down the axis
|
|
toward the origin.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetrotation,nodeSetRotation>>, <<nodemove,nodeMove>>, <<nodesetquaternion,nodeSetQuaternion>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The logo turns half a degree a frame about its own up axis.
|
|
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)
|
|
----
|
|
|
|
Turns the node to face the camera every frame. `BILLBOARD_ALL` faces it
|
|
squarely, taking all three axes from the camera; `BILLBOARD_Y` turns about
|
|
world up only, so the node stays upright (trees, health bars);
|
|
`BILLBOARD_NONE`, the default, does not turn at all. The node's world
|
|
position and scale are kept and its own rotation is ignored while a mode is
|
|
set. Any other value aborts the script. It works on meshes as well as
|
|
sprites and text, and turns to whichever camera is drawing: a view sees
|
|
the billboard face on too, while its shadow follows the window's camera.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetsprite,nodeSetSprite>>, <<nodesettext,nodeSetText>>, <<cameraset,cameraSet>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A name over the fox facing the camera squarely, and a crate icon that
|
|
-- turns about its own up axis only.
|
|
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)
|
|
----
|
|
|
|
[#nodesetmaterial]
|
|
==== nodeSetMaterial
|
|
|
|
[source,text]
|
|
----
|
|
nodeSetMaterial(node, material)
|
|
nodeSetMaterial(node, nil)
|
|
----
|
|
|
|
Puts a material on whatever mesh the node has, keeping the mesh. `nil`
|
|
removes it, leaving the default look: white, half rough, untextured. Use
|
|
it to restyle a model's part, to swap a highlight material in and out, or
|
|
to dress a mesh the engine made for you, such as a rope's tube from
|
|
`softNew`. A material handle that does not exist aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmesh,nodeSetMesh>>, <<materialnew,materialNew>>, <<softnew,softNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The rope hangs from the crane; the engine makes its tube, we dress it.
|
|
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)
|
|
----
|
|
|
|
Gives the node a mesh to draw, with a material or, when omitted, the
|
|
default look (white, half rough). Both arguments must be live handles; a
|
|
missing mesh or material aborts the script. Any node can draw, including a
|
|
node inside a model instance, and any number of nodes can share one mesh.
|
|
The node's morph weights are sized to the new mesh's targets and start at
|
|
`0`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshbox,meshBox>>, <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>, <<nodesetvisible,nodeSetVisible>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A car body with four wheels, each a cylinder turned so its X is the axle.
|
|
local car = nodeNew()
|
|
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
|
|
nodeSetPosition(car, 0, 0.9, 0)
|
|
local tyreMesh = 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 tyre = nodeNew(wheel)
|
|
nodeSetMesh(tyre, tyreMesh, rubber)
|
|
nodeSetRotation(tyre, 0, 0, 90)
|
|
end
|
|
----
|
|
|
|
[#nodesetmorph]
|
|
==== nodeSetMorph
|
|
|
|
[source,text]
|
|
----
|
|
nodeSetMorph(node, nameOrIndex, weight)
|
|
----
|
|
|
|
Sets how much of one morph target (blend shape) the node's mesh shows,
|
|
by name or by number from `1`; weights are usually `0` to `1` and several
|
|
targets add up. The node is the one carrying the mesh, which for a model is
|
|
a child of the instance root (`nodeFind` by the mesh's name, or
|
|
`nodeGetChildren`). A target the mesh does not have aborts the script.
|
|
An animation with a weights channel writes the weights every frame while
|
|
it plays, overriding what you set.
|
|
|
|
*Notes:* Up to eight targets with non-zero weights apply per mesh in a
|
|
frame; with more, the eight strongest by absolute weight are used.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodegetmorph,nodeGetMorph>>, <<nodegetmorphs,nodeGetMorphs>>, <<animationplay,animationPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Lip sync from a table of mouth openings per 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)
|
|
----
|
|
|
|
Names the node so `nodeFind` can find it later, replacing any name it had
|
|
(a model node's glTF name included). Names need not be unique; `nodeFind`
|
|
returns the first match in depth-first order.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodefind,nodeFind>>, <<nodegetname,nodeGetName>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Spawned crates are named so the level script can find them by slot.
|
|
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)
|
|
----
|
|
|
|
Moves the node, with its whole subtree, under another parent, becoming
|
|
that parent's last child. The node's local position, rotation, and scale
|
|
are kept as they are, so it now sits at that offset from the new parent and
|
|
moves with it; pass `0` to return it to the root. The root cannot be moved,
|
|
and a node cannot go under itself or one of its own descendants; either
|
|
aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodegetparent,nodeGetParent>>, <<nodenew,nodeNew>>, <<nodesetposition,nodeSetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Picking up a crate: it rides in the hand from now on, set down later.
|
|
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)
|
|
----
|
|
|
|
Places the node at `x, y, z` in its parent's space; for a node under the
|
|
root that is world space. Children come along. A node with a dynamic body
|
|
on it is moved by the physics afterward; set its position before
|
|
`bodyNew`, or teleport it through the body calls.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodemove,nodeMove>>, <<nodegetposition,nodeGetPosition>>, <<nodesetrotation,nodeSetRotation>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Crates dropped along a ridge, each six units above the ground there.
|
|
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)
|
|
----
|
|
|
|
Sets the node's rotation relative to its parent as a quaternion, for
|
|
values that came from `nodeGetQuaternion`, a model, or your own math. The
|
|
quaternion is normalized on the way in, so it need not be unit length.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The ghost copies the hero's pose exactly, a beat behind.
|
|
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)
|
|
----
|
|
|
|
Sets the node's rotation relative to its parent as Euler angles in
|
|
degrees, replacing whatever it had: the node is turned about its Y (yaw)
|
|
first, then tilted about its X (pitch), then rolled about its Z. Positive
|
|
angles turn counter-clockwise looking down the axis toward the origin, so
|
|
`nodeSetRotation(plane, 90, 0, 0)` tips a floor plane up to face +Z.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<noderotate,nodeRotate>>, <<nodesetquaternion,nodeSetQuaternion>>, <<nodelookat,nodeLookAt>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A monitor screen standing on a stand, facing the camera at +Z.
|
|
local monitor = nodeNew()
|
|
nodeSetMesh(monitor, meshPlane(2.0, 1.5), screen)
|
|
nodeSetPosition(monitor, -3, 2.1, 1.06)
|
|
nodeSetRotation(monitor, 90, 0, 0)
|
|
-- A ramp tilted 25 degrees about Z.
|
|
local ramp = nodeNew()
|
|
nodeSetMesh(ramp, meshBox(6, 0.2, 3), asphalt)
|
|
nodeSetRotation(ramp, 0, 0, 25)
|
|
----
|
|
|
|
[#nodesetscale]
|
|
==== nodeSetScale
|
|
|
|
[source,text]
|
|
----
|
|
nodeSetScale(node, scale)
|
|
nodeSetScale(node, x, y, z)
|
|
----
|
|
|
|
Scales the node and everything under it, uniformly with one factor or per
|
|
axis with three. Loaded models are often in centimeters or meters; scale
|
|
the instance root once to fit your scene and place things under it in the
|
|
model's own units. Sprites and text on the node are scaled too, on top of
|
|
their `height`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodegetscale,nodeGetScale>>, <<modelinstance,modelInstance>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The fox is modeled in centimeters: a hundredth brings it to meters.
|
|
local fox = modelInstance(foxModel)
|
|
nodeSetParent(fox, player)
|
|
nodeSetScale(fox, 0.01)
|
|
nodeSetRotation(fox, 0, 90, 0)
|
|
-- A flattened sphere for a puddle.
|
|
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)
|
|
----
|
|
|
|
Whether the node's mesh is drawn into shadow maps; `true` for every new
|
|
node. Turn it off for the mesh round a light (a bulb, a flame) so it does
|
|
not shadow its own light, and for anything that glows. The node still
|
|
draws and still receives shadows from others. It applies to the node only,
|
|
not its children.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightsetshadow,lightSetShadow>>, <<nodesetvisible,nodeSetVisible>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A bulb inside its shade: the glass casts nothing, the shade does.
|
|
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)
|
|
----
|
|
|
|
Shows a loaded sprite on the node: a quad `height` world units tall
|
|
(default `1`) and as wide as the picture's aspect makes it, blended by the
|
|
picture's alpha, drawn from both sides, and lit by the scene unless `lit`
|
|
is `false`. Every frame of an animated sprite is uploaded and the first
|
|
shows; `nodeSetSpriteFrame` picks another. The quad becomes the node's
|
|
mesh with a private material, replacing any mesh, sprite, or text it had;
|
|
`nil` or no second argument takes the picture off and leaves the node
|
|
empty. It aborts the script when the 3D renderer is unavailable.
|
|
|
|
*Parameters:*
|
|
|
|
* `sprite` -- a handle from `spriteLoad`, or `nil` to clear.
|
|
* `height` -- the quad's height in world units, default `1`; the node's scale multiplies it.
|
|
* `lit` -- `true` (default) shades the picture with the scene's lights and shadows; `false` shows it as is.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetspriteframe,nodeSetSpriteFrame>>, <<nodesetbillboard,nodeSetBillboard>>, <<spriteload,spriteLoad>>, <<nodesettext,nodeSetText>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A crate icon floating over the fox, turning to face the player.
|
|
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)
|
|
-- An unlit glow that reads the same in shadow.
|
|
local flare = nodeNew(torchTip)
|
|
nodeSetSprite(flare, spriteLoad("flare.png"), 0.3, false)
|
|
nodeSetBillboard(flare, BILLBOARD_ALL)
|
|
----
|
|
|
|
[#nodesetspriteframe]
|
|
==== nodeSetSpriteFrame
|
|
|
|
[source,text]
|
|
----
|
|
nodeSetSpriteFrame(node, frame)
|
|
----
|
|
|
|
Which frame of the node's animated sprite shows, counted from `0`. The
|
|
node's sprite does not advance on its own; drive it from
|
|
`onOverlayUpdate`. A node without a sprite, or a frame it does not have,
|
|
aborts the script; text and a still sprite have only frame `0`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetsprite,nodeSetSprite>>, <<spriteload,spriteLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A campfire sprite cycling its frames at ten a second.
|
|
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)
|
|
----
|
|
|
|
Shows a line of text on the node, rendered in the selected font
|
|
(`fontSelect`) at the current quality (`fontQuality`) in the foreground
|
|
color (`colorForeground`), on a quad `height` world units tall (default
|
|
`1`) and as wide as the rendered text makes it. It is unlit, blended, and
|
|
drawn from both sides: name tags, signs, a score over a table. Like a
|
|
sprite it becomes the node's mesh, replacing any mesh, sprite, or text
|
|
there; call again to change the text, `nil` or no second argument to take
|
|
it off. With no font selected, or without the 3D renderer, it aborts the
|
|
script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetsprite,nodeSetSprite>>, <<nodesetbillboard,nodeSetBillboard>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A fixed sign by the lamp, and a score that updates when it changes.
|
|
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)
|
|
----
|
|
|
|
Hides or shows the node and everything under it; a child's own flag still
|
|
applies when its parent is shown again. Hidden meshes are not drawn and
|
|
hidden lights stop shining. Use it to keep a marker or a spare prop ready
|
|
without deleting and rebuilding it.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A marker that appears where the ball last hit the paddle.
|
|
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
|
|
|
|
The overlay is a 32-bit RGBA surface composited over the video every frame; all 2D drawing lands on it. Its coordinates are the overlay resolution set by `overlaySetResolution`, which defaults to half the video's width and height, and are scaled to the window at display time. Draw from `onOverlayUpdate` (or from `singeMain` in the <<threaded,Threaded>> model) and return `OVERLAY_UPDATED` when something changed. The `overlay*` primitives draw one-pixel outlines in the current foreground color and silently clip whatever falls outside the surface. `overlayPrint` is the exception: it takes character-cell coordinates and copies the built-in console font as is, ignoring the color settings.
|
|
|
|
[#overlaybanner]
|
|
==== overlayBanner
|
|
|
|
[source,text]
|
|
----
|
|
overlayBanner(text [, height])
|
|
----
|
|
|
|
Hypseus extension. Shows a short message over the picture for about thirty drawn frames and then takes it away again. `height` is where it sits, as a per cent down the picture, from 1 to 95; the default is 47, which is Hypseus's own, and a number outside that range is replaced by the default rather than refused.
|
|
|
|
A message longer than 60 characters is ignored, exactly as Hypseus ignores it. The banner is drawn by the same shipped document the subtitles use (`Singe/subtitle.rml`, styled by `Singe/subtitle.rcss`), so it looks like a subtitle and is styled with them; while it is up it takes the place of any subtitle, and the subtitle comes back when it times out. The text is drawn as text, so `<`, `>` and `&` reach the screen as themselves.
|
|
|
|
*Parameters:*
|
|
|
|
* `text` -- string; up to 60 characters.
|
|
* `height` -- optional number; 1 to 95, a per cent down the picture. Default 47.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<srtload,srtLoad>>, <<subtitles,Subtitles>>, <<overlayprint,overlayPrint>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Tell the player what just happened without building a HUD for it.
|
|
overlayBanner("Extra life!", 20)
|
|
----
|
|
|
|
[#overlaybox]
|
|
==== overlayBox
|
|
|
|
[source,text]
|
|
----
|
|
overlayBox(x1, y1, x2, y2)
|
|
----
|
|
|
|
Draws the outline of the axis-aligned rectangle with opposite corners `(x1, y1)` and `(x2, y2)`, as four one-pixel lines in the foreground color. Both corners are inclusive and may be given in any order. There is no filled variant; fill an area with `overlayClear` on a background color, or with a stretched sprite.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Frame the overlay in red and outline each platform block.
|
|
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)
|
|
----
|
|
|
|
Draws a one-pixel circle outline in the foreground color, centered on `(x, y)`, using the midpoint algorithm. A radius of `0` or less draws nothing. Coordinates and radius are integers in overlay units; fractions are truncated.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- center in overlay coordinates.
|
|
* `radius` -- radius in overlay units.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayellipse,overlayEllipse>>, <<overlayplot,overlayPlot>>, <<colorforeground,colorForeground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A gun-sight crosshair that turns red while the player is hit.
|
|
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()
|
|
----
|
|
|
|
Fills the whole overlay with the current background color. The background starts as transparent black, so with no `colorBackground` call this erases the overlay back to invisible and the video shows through. A partly opaque background dims the video; a fully opaque one hides it. Most games call it first thing in `onOverlayUpdate`. Any arguments are ignored.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Dim the paused video to half brightness behind the service menu.
|
|
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)
|
|
----
|
|
|
|
Draws a one-pixel ellipse outline in the foreground color, inscribed in the axis-aligned rectangle with opposite corners `(x1, y1)` and `(x2, y2)`. The corners may be given in any order. A rectangle of equal width and height gives a circle, so this is also the way to draw a circle from its bounding box rather than its center.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlaycircle,overlayCircle>>, <<overlaybox,overlayBox>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Highlight the selected cabinet with an oval that fits its panel.
|
|
function onOverlayUpdate()
|
|
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
|
|
if selected == CABINET then
|
|
colorForeground(255, 220, 0, 255)
|
|
overlayEllipse(CABINET_X - 6, CABINET_Y - 6, CABINET_X + CABINET_W + 6, CABINET_Y + CABINET_H + 6)
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#overlaygetheight]
|
|
==== overlayGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
result = overlayGetHeight()
|
|
----
|
|
|
|
Returns the height of the overlay surface in overlay units: the value last given to `overlaySetResolution`, or half the video height before any call. Read it at startup, after setting the resolution, to lay out positions in terms of the actual drawing area. Any arguments are ignored.
|
|
|
|
*Returns:* integer height.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<overlaygetwidth,overlayGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Anchor the text panel to the bottom margin whatever the overlay size is.
|
|
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()
|
|
----
|
|
|
|
Returns the width of the overlay surface in overlay units: the value last given to `overlaySetResolution`, or half the video width before any call. Use it to right-align or center elements instead of hard-coding a width. Any arguments are ignored.
|
|
|
|
*Returns:* integer width.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<overlaygetheight,overlayGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Right-align the marquee and center the title, independent of resolution.
|
|
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)
|
|
----
|
|
|
|
Draws a one-pixel Bresenham line in the foreground color from `(x1, y1)` to `(x2, y2)`, both endpoints included. Pixels outside the overlay are dropped, so endpoints may lie off the surface. Coordinates are truncated to integers.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayplot,overlayPlot>>, <<overlaybox,overlayBox>>, <<colorforeground,colorForeground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw the laser from the gun to the aim point, and a simple aim reticle.
|
|
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)
|
|
----
|
|
|
|
Sets one overlay pixel to the foreground color. A point outside the surface is ignored. Each call locks and unlocks the overlay, so plotting thousands of points per frame from Lua is slow; use a sprite for anything with structure and keep this for stars, sparks, and debugging marks.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A slowly twinkling star field behind the title.
|
|
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)
|
|
----
|
|
|
|
Prints a string with the engine's built-in fixed-width console font. Unlike every other drawing call, the position is a character cell: the engine multiplies `column` and `row` by the glyph width and height, so `(0, 0)` is the top-left cell and `(1, 0)` is one glyph to the right. Glyphs are copied from the font image as they are; the current foreground and background colors are not applied, and only the font's own background is transparent. Each byte of the string selects one of the font's 256 glyphs, so the text is treated as bytes, not UTF-8. Characters that would run past the right edge of the overlay are dropped, and text starting beyond the edge draws nothing.
|
|
|
|
*Parameters:*
|
|
|
|
* `column`, `row` -- character cell, `0` at the top left.
|
|
* `text` -- the string to print.
|
|
|
|
*Notes:* The console font is meant for diagnostics and service screens. Use `fontPrint` or `fontToSprite` for colored or proportional text.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontprint,fontPrint>>, <<overlayclear,overlayClear>>, <<colorbackground,colorBackground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Diagnostics in the corner while a download completes.
|
|
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 extension. Shows the overlay in grey: before each upload the engine converts a copy of it to luma with the BT.601 weights, keeping the alpha channel as it is (Hypseus thresholds it). The overlay's own pixels stay in colour, so nothing the script reads back changes, and drawing goes on as usual. Pair it with `vldpSetMonochrome` for a black and white mode. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpsetmonochrome,vldpSetMonochrome>>, <<overlayclear,overlayClear>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- One setting drives both the disc and the overlay.
|
|
function applyChroma(colour)
|
|
vldpSetMonochrome(not colour)
|
|
overlaySetMonochrome(not colour)
|
|
end
|
|
----
|
|
|
|
[#overlaysetresolution]
|
|
==== overlaySetResolution
|
|
|
|
[source,text]
|
|
----
|
|
overlaySetResolution(width, height)
|
|
----
|
|
|
|
Replaces the overlay with a new surface of the given size and discards everything drawn so far. From then on every overlay coordinate, sprite position, and `overlayGetWidth` or `overlayGetHeight` result is in the new space, which the engine scales to the window at display time. Call it once during startup, before drawing; the default is half the video resolution in each dimension. A width or height of `0` or less ends the script with an error. Higher resolutions give finer placement and sharper text for more compositing work per frame; the disc's native size from `discGetWidth` and `discGetHeight` is the common choice.
|
|
|
|
*Parameters:*
|
|
|
|
* `width` -- new overlay width in pixels, greater than `0`.
|
|
* `height` -- new overlay height in pixels, greater than `0`.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlaygetwidth,overlayGetWidth>>, <<overlaygetheight,overlayGetHeight>>, <<discgetwidth,discGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Match the disc, then lay the menu out in that coordinate space.
|
|
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
|
|
----
|
|
|
|
[#setoverlaylinearscale]
|
|
==== setOverlayFullAlpha
|
|
|
|
[source,text]
|
|
----
|
|
setOverlayFullAlpha()
|
|
----
|
|
|
|
Hypseus extension, accepted and ignored. Hypseus blits its overlay with the alpha channel thresholded at `128`, so a half-transparent pixel is either fully opaque or gone, and this asks it to upload the surface as it stands and blend properly instead. Singe's overlay has always been full RGBA with real per-pixel alpha, so what this asks for is already in force.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00
|
|
|
|
==== setOverlayLinearScale
|
|
|
|
[source,text]
|
|
----
|
|
setOverlayLinearScale(enabled)
|
|
----
|
|
|
|
Hypseus extension. Chooses how the overlay is filtered on its way to the window. With `true`, the default and what a reload restores, it is sampled linearly, so a low resolution overlay softens rather than blocking up; with `false` the nearest pixel is taken, which keeps pixel art, one-pixel outlines and small text crisp. Linear is the default because it is how every Singe release has drawn the overlay; Hypseus defaults to the nearest pixel instead, so a game moved from Hypseus that never calls this will look slightly softer until it does. Nothing about the drawing changes, only the final scale from the overlay resolution to the window, so everything on the overlay -- the primitives, fonts, sprites and the 3D layer -- goes through the one filter together, and the disc picture behind it is untouched. The setting survives `overlaySetResolution`, which replaces the overlay. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<overlaysetresolution,overlaySetResolution>>, <<spritequality,spriteQuality>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A 320x240 overlay on a 4K screen; smooth the artwork, sharpen the text.
|
|
overlaySetResolution(320, 240)
|
|
setOverlayLinearScale(true)
|
|
...
|
|
setOverlayLinearScale(false)
|
|
overlayPrint(1, 1, "CREDITS 2")
|
|
----
|
|
|
|
[#setoverlayontop]
|
|
==== setOverlayOnTop
|
|
|
|
[source,text]
|
|
----
|
|
loaded, id = setOverlayOnTop(enabled)
|
|
----
|
|
|
|
Hypseus extension. Chooses whether the overlay and the GUIs drawn over the picture are above the bezel artwork or below it. Only artwork drawn in front of the picture (`--bezelflip`) is ever above them, so with the artwork behind -- which is the default -- this changes nothing. `false` is the setting a game starts with, and Hypseus's own default. It is remembered until the script reloads.
|
|
|
|
The setting is only recorded while artwork is loaded; without it the call does nothing and answers `false` and `"0"`, exactly as Hypseus does. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean; `true` puts the overlay and the GUIs above the artwork.
|
|
|
|
*Returns:* boolean, whether artwork is loaded, and its identifier as a string (`"0"` when none is).
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<mainbezelloaded,mainBezelLoaded>>, <<guidrawscreen,guiDrawScreen>>, <<bezels,Bezels>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The cabinet frame overlaps the picture, but the score line has to stay readable.
|
|
if mainBezelLoaded() then
|
|
setOverlayOnTop(true)
|
|
end
|
|
----
|
|
|
|
[#setoverlayopacity]
|
|
==== setOverlayOpacity
|
|
|
|
[source,text]
|
|
----
|
|
setOverlayOpacity(alpha)
|
|
----
|
|
|
|
Hypseus extension. How solid the whole overlay is, from `0` (invisible) to `255` (as the game drew it). It multiplies whatever alpha the overlay's own pixels already carry, so a half transparent sprite stays half as solid as the rest; it does not change the overlay's contents, and nothing has to be redrawn.
|
|
|
|
The level is clamped to `0` to `255` rather than refused, and a script starts at `255`. It is remembered until the script reloads, and it survives a change of overlay resolution.
|
|
|
|
The GUIs, the 3D scene and the bezel artwork are drawn separately and are not affected.
|
|
|
|
*Parameters:*
|
|
|
|
* `alpha` -- number; `0` to `255`.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<setoverlaylinearscale,setOverlayLinearScale>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fade the whole heads-up display out over half a second.
|
|
local fade = 255
|
|
function onOverlayUpdate()
|
|
fade = math.max(0, fade - 8)
|
|
setOverlayOpacity(fade)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#physicsref]
|
|
=== Physics
|
|
|
|
World-level calls: the ray query, gravity, the 2D mode, pausing and debug drawing. Physics is initialized with the engine, so there is nothing to enable before the first `bodyNew`; the world steps sixty times a second from the wall clock, after animations and before the scene renders. Positions and distances are in world units and gravity in units per second squared. See <<physics,Physics>> in Game Development for how bodies, nodes and the frame fit together.
|
|
|
|
[#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)
|
|
----
|
|
|
|
Finds the first body along a ray from `(x, y, z)` in the direction `(dx, dy, dz)`, within `maxDistance` world units. The direction is normalized, so any length works, but a zero direction hits nothing. Triggers and players are hit like any other body; a player is reported by its node. With `sceneUnproject` supplying two points along the mouse, this is picking; straight down from a point, it is a height probe.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y`, `z` -- the ray's start in world space.
|
|
* `dx`, `dy`, `dz` -- its direction; only the direction matters.
|
|
* `maxDistance` -- how far to look; default `1000`, and `0` or less means the default.
|
|
|
|
*Returns:* The body's node, the hit point, and the surface normal there, or a single `nil` when nothing is hit.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneunproject,sceneUnproject>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<linedraw,lineDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Drop the marker onto whatever level ground is under the cursor.
|
|
function onOverlayUpdate()
|
|
local mx, my = mouseGetPosition(0)
|
|
local ox, oy, oz = sceneUnproject(mx, my, 0)
|
|
local fx, fy, fz = sceneUnproject(mx, my, 10)
|
|
local hit, hx, hy, hz, nx, ny, nz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz, 60)
|
|
nodeSetVisible(marker, hit ~= nil and ny > 0.7)
|
|
if hit then
|
|
nodeSetPosition(marker, hx, hy + 0.05, hz)
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#physicsset2d]
|
|
==== physicsSet2D
|
|
|
|
[source,text]
|
|
----
|
|
physicsSet2D(planar)
|
|
----
|
|
|
|
With `true`, bodies made from now on move only in X and Y and turn only about Z: a 2D game's world, usually in overlay coordinates with gravity pointing +Y so things fall down the screen. Bodies already made keep their freedom, so call it before any `bodyNew`; `false` returns to full 3D for bodies made afterward. Players are different: while the mode is on, every player, whenever it was made, runs along X with its Z held at `0` and `playerMove` takes a single value. The 2D world works without a GPU, the Raspberry Pi 3 included.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<physicssetgravity,physicsSetGravity>>, <<bodynew,bodyNew>>, <<playermove,playerMove>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A platformer world in overlay pixels: gravity down the screen, a floor and walls.
|
|
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)
|
|
----
|
|
|
|
Draws what the simulation holds as lines over the scene each frame, for as long as the mask is set: `DEBUG_SHAPES` for every moving body's shape in wireframe (soft bodies included, and enabled players in yellow), `DEBUG_CONSTRAINTS` for joints, ragdoll joints and vehicle constraints with their limits, `DEBUG_CONTACTS` for a red cross where bodies met this step, and `DEBUG_VELOCITIES` for a line along each moving body's velocity. Add the flags together; `DEBUG_ALL` is all four. Static bodies' shapes stay off unless `DEBUG_STATIC` is added, because a level mesh is a lot of lines. `DEBUG_NONE` turns it off, and nothing is drawn or costs while it is off.
|
|
|
|
*Notes:* A build of Singe made without Jolt's debug renderer prints a message to the console and draws nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<linedraw,lineDraw>>, <<navdraw,navDraw>>, <<physicssetenabled,physicsSetEnabled>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Toggle the physics overlay with a debug key; static shapes too, since the level is small.
|
|
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)
|
|
----
|
|
|
|
Pauses the simulation with `false` and resumes it with `true`. While paused no step runs: dynamic bodies hold still and keep their velocities, kinematic bodies stop following their nodes, players stop moving, and no `onCollision` or `onTrigger` fires. Time does not accumulate while paused, so resuming does not replay the missed steps. The game's own pause does the same without this call; use it for a menu, a cutscene or a freeze-frame that leaves the rest of the game running.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<physicssetgravity,physicsSetGravity>>, <<bodyisresting,bodyIsResting>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Freeze the world while the inventory is open.
|
|
function openInventory()
|
|
inventoryOpen = true
|
|
physicsSetEnabled(false)
|
|
end
|
|
|
|
function closeInventory()
|
|
inventoryOpen = false
|
|
physicsSetEnabled(true)
|
|
end
|
|
----
|
|
|
|
[#physicssetgravity]
|
|
==== physicsSetGravity
|
|
|
|
[source,text]
|
|
----
|
|
physicsSetGravity(x, y, z)
|
|
----
|
|
|
|
Sets the acceleration every dynamic body and player feels, in world units per second squared along each world axis. The default is `0, -9.81, 0`, Earth's gravity in meters; `0, 0, 0` is space, and a 2D game in overlay pixels wants something like `0, 900, 0` since overlay Y runs down the screen. The direction opposite to gravity is "up" for players: the way they stand, jump and climb steps.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodynew,bodyNew>>, <<physicsset2d,physicsSet2D>>, <<playersetgravityscale,playerSetGravityScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The moon level: a sixth of Earth's pull, and a longer jump to match.
|
|
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]
|
|
=== Player
|
|
|
|
A player is a character controller on a node: where a body goes where forces send it, a player goes where the script says, walking floors, climbing steps, sliding along walls, riding platforms and shoving light bodies aside. Players are addressed by their node, and a node carries a body or a player, not both. Sizes and speeds are in world units (meters in a 3D scene, overlay pixels in a 2D world) and the controller's tolerances scale with the shape, so a 30 pixel capsule behaves like a 0.3 meter one. The player drives its node's position each step and leaves the rotation to the script. Every call here raises an error when the node has no player. See <<physics,Physics>> in Game Development, Players.
|
|
|
|
[#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)
|
|
----
|
|
|
|
Puts a player on the node, where the node is in the world right now: a capsule of that radius and total height, or any convex `bodyNew` shape with its sizes, scaled by the node's world scale. The shape stands on the node's origin, so the node is the feet. It starts with a mass of `70` kilograms, a push of `300` newtons, a slope limit of `45` degrees, a step height of `0.3` scaled by the shape, gravity scale `1` and the default swimming rates. Replaces any body or player the node had. Raises an error for `SHAPE_MESH` (a player needs a convex shape; use `SHAPE_HULL`), when physics is not available on this machine, or when the player cannot be made (a hull with too little mesh, or sixteen players already).
|
|
|
|
*Parameters:*
|
|
|
|
* `radius`, `height` -- a capsule, radius and total height along up, caps included.
|
|
* `SHAPE_*` -- a shape constant followed by its `bodyNew` sizes; `SHAPE_HULL` takes none and wraps the node's mesh and everything under it.
|
|
|
|
*Notes:* A call with exactly three arguments is always a capsule, so a sphere needs a fourth argument (any number) after its radius to be read as `SHAPE_SPHERE`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playermove,playerMove>>, <<playerdelete,playerDelete>>, <<bodynew,bodyNew>>, <<nodesetparent,nodeSetParent>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The hero: a capsule with the Fox model hanging off the node, feet on the ground.
|
|
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)
|
|
-- A robot is a box; it turns square corners and cannot roll.
|
|
robot = nodeNew()
|
|
playerNew(robot, SHAPE_BOX, 0.8, 1.6, 0.6)
|
|
----
|
|
|
|
[#playerdelete]
|
|
==== playerDelete
|
|
|
|
[source,text]
|
|
----
|
|
playerDelete(node)
|
|
----
|
|
|
|
Removes the player; the node and whatever hangs under it stay where they are. Deleting the node removes its player too, and so does giving the node a body with `bodyNew`. Raises an error when the node has no player.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playernew,playerNew>>, <<playersetenabled,playerSetEnabled>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The hero climbs into the car: the controller goes, the model rides along.
|
|
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)
|
|
----
|
|
|
|
Tells the player the velocity it wants this frame, in world units per second. On the ground or in the air the two horizontal values are used as given, on top of whatever platform it stands on; the vertical part comes from gravity, the ground and `playerJump`, and `vy` is ignored. When the player is swimming all three values steer it, and the water eases its velocity toward them at the drag from `playerSetSwim`, sinking at the sink speed when `vy` is `0`. In a 2D world a single `vx` runs along X and any `vz` is ignored. Call it every frame from `onOverlayUpdate`: the request is consumed by that frame's steps, and a frame without one stands still.
|
|
|
|
*Parameters:*
|
|
|
|
* `vx`, `vz` -- the horizontal velocity wanted, world units per second (2D: overlay pixels along X).
|
|
* `vy` -- the vertical velocity wanted while swimming; positive is up.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playerjump,playerJump>>, <<playersetvelocity,playerSetVelocity>>, <<playerisswimming,playerIsSwimming>>, <<physicsset2d,physicsSet2D>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Walk with the left stick, swim up while the button is held, face the way of travel.
|
|
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)
|
|
----
|
|
|
|
Whether the player was in water at the last step: the middle of its shape below the surface of a `bodySetWater` volume. While swimming, `playerMove` steers in three axes, gravity gives way to the sink speed, and the water's current carries the player.
|
|
|
|
*Returns:* Boolean.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetswim,playerSetSwim>>, <<bodysetwater,bodySetWater>>, <<playermove,playerMove>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Switch the animation and the footstep sounds when the hero goes in.
|
|
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)
|
|
----
|
|
|
|
Asks the player to leave the ground at that upward speed, in world units per second, at the next step. It is granted only when there is walkable ground underfoot right now, so holding a button down does not bounce the player and a jump in mid-air does nothing. A negative speed is treated as `0`. The player keeps its horizontal velocity through the jump and `playerMove` still steers it in the air.
|
|
|
|
*Returns:* `true` when the jump was granted.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playerisonground,playerIsOnGround>>, <<playermove,playerMove>>, <<playersetgravityscale,playerSetGravityScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Jump on the action button, with a sound only when it actually happened.
|
|
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)
|
|
----
|
|
|
|
Sets how the player behaves in water: how fast it sinks, in world units per second, when `playerMove` asks for no vertical movement (default `0.3`), and how quickly the water pulls its velocity toward what `playerMove` asks, as a rate per second (default `2`, so most of the difference is gone within a second; `0` never changes velocity). Both are clamped to at least `0`. A heavy diver sinks fast with a high drag; a cork sinks not at all.
|
|
|
|
*Parameters:*
|
|
|
|
* `sinkSpeed` -- the drift downward with no vertical input, units per second.
|
|
* `drag` -- the rate the water pulls velocity toward the request, per second.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playerisswimming,playerIsSwimming>>, <<playermove,playerMove>>, <<bodysetwater,bodySetWater>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The suit of armor drags the hero down; take it off to float.
|
|
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)
|
|
----
|
|
|
|
Sets the player's whole velocity, in world units per second along each world axis, and drops any `playerMove` request made this frame. The vertical part is what lasts: an upward speed lifts the player off the ground and it falls back under gravity, which is a launch pad or a bounce. The horizontal part holds only until the next step, when it is replaced by what `playerMove` asks (nothing, when it is not called), because a player has no horizontal momentum of its own. A knock-back or a dash is therefore an upward kick here plus a `playerMove` velocity fed over the following frames.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playergetvelocity,playerGetVelocity>>, <<playermove,playerMove>>, <<playerjump,playerJump>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A hit from the ogre throws the hero up and, over the next frames, away from it.
|
|
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)
|
|
----
|
|
|
|
The player's velocity after the last step, in world units per second along each world axis, including what the ground it rides on gave it. Read the vertical part to tell rising from falling, or the whole thing for a landing thud.
|
|
|
|
*Returns:* Three numbers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetvelocity,playerSetVelocity>>, <<playerisonground,playerIsOnGround>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fall damage from the speed the hero hit the ground at.
|
|
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)
|
|
----
|
|
|
|
Whether the player is standing on walkable ground as of the last step: a surface no steeper than `playerSetSlope`. In the air or on a slope too steep to climb it is `false`. This is the same test `playerJump` makes.
|
|
|
|
*Returns:* Boolean.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playergetground,playerGetGround>>, <<playerjump,playerJump>>, <<playersetslope,playerSetSlope>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Coyote time: a jump is still allowed for a few frames after walking off an edge.
|
|
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)
|
|
----
|
|
|
|
What the player is standing on and the ground's surface normal there. The node is the body underfoot, walkable or not; it is `nil` in the air, and the normal is then `0, 1, 0`. Use the node to tell a moving platform, ice or a pressure plate from the floor, and the normal to lean a model into a slope.
|
|
|
|
*Returns:* The node stood on or `nil`, then the normal.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playerisonground,playerIsOnGround>>, <<bodysetfriction,bodySetFriction>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Footsteps sound different on the wooden bridge, and the plate opens the gate.
|
|
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)
|
|
----
|
|
|
|
The tallest step the player climbs without jumping, in world units; anything higher is a wall it slides along. The default is `0.3` for a shape whose smallest half-size is `0.3` (a capsule of radius `0.3`), scaling with the shape, so a 30 pixel player in a 2D world starts at 30 pixels. The same distance is how far down the player sticks to the floor over the lip of a step, so a bigger step also keeps it glued down slopes. Negative values are clamped to `0`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetslope,playerSetSlope>>, <<playernew,playerNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Stairs in this level rise 0.35 a tread; make sure the hero takes them.
|
|
playerNew(hero, 0.3, 1.0)
|
|
playerSetStep(hero, 0.4)
|
|
-- The 2D platformer's ledges are 26 pixels.
|
|
playerNew(runner, 16, 48)
|
|
playerSetStep(runner, 26)
|
|
----
|
|
|
|
[#playersetslope]
|
|
==== playerSetSlope
|
|
|
|
[source,text]
|
|
----
|
|
playerSetSlope(node, degrees)
|
|
----
|
|
|
|
The steepest slope the player walks up, in degrees from level; steeper ground does not count as standing, so the player slides down it and cannot jump from it. The default is `45`, and the value is clamped between `0` and `89`. A low value makes hills into obstacles, a high one lets the player climb almost anything short of a wall.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetstep,playerSetStep>>, <<playerisonground,playerIsOnGround>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The mountain goat climbs where the hero slides.
|
|
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)
|
|
----
|
|
|
|
The player's mass, used when it shoves dynamic bodies and as its weight on any it stands on. The default is `70` kilograms; values below `0.001` are raised to that. Mass has no effect on how the player falls or walks, only on the shoving, so pair it with `playerSetPush`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetpush,playerSetPush>>, <<bodysetmass,bodySetMass>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The troll barges through what the hero has to walk around.
|
|
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)
|
|
----
|
|
|
|
How hard the player shoves dynamic bodies it walks into, in newtons. The default `300` moves crates and balls but not cabinets; `0` walks around everything as if it were static. Negative values are clamped to `0`. A body's own `bodySetMass` decides what a given strength can move.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetmass,playerSetMass>>, <<bodysetmass,bodySetMass>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The strength potion lets the hero move the boulder for ten seconds.
|
|
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)
|
|
----
|
|
|
|
Multiplies the world's gravity for this player alone: `1` (the default) falls like everything else, `0` floats where it is (a ladder, a zero-gravity room, a flying mount), more falls faster for a heavy feel, and less makes a floaty jump. It does not change what `playerJump` does at takeoff, only how quickly the player comes back down. While swimming the water's sink speed takes gravity's place.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<physicssetgravity,physicsSetGravity>>, <<playerjump,playerJump>>, <<playersetswim,playerSetSwim>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- On the ladder the hero climbs with the stick instead of falling.
|
|
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)
|
|
----
|
|
|
|
Teleports the player to a world position, touching nothing on the way, and stops it dead. The node moves with it and keeps its rotation. Use it for spawning, respawning and level transitions; moving the node with `nodeSetPosition` instead leaves the player where it was and the node is pulled back at the next step.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playersetvelocity,playerSetVelocity>>, <<nodesetposition,nodeSetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Back to the last checkpoint after a fall into the pit.
|
|
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)
|
|
----
|
|
|
|
With `false` the player leaves the simulation: its body is removed from the world, so it does not move, fall, drive its node or block anything, `onTrigger` reports it leaving every trigger it stood in, and it is left out of the debug drawing. `true` puts the body back and picks it up again from wherever the node is. Disable a player while a ragdoll, a cutscene or a vehicle owns the character.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<playerdelete,playerDelete>>, <<ragdollnew,ragdollNew>>, <<bodysetenabled,bodySetEnabled>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A hard hit hands the hero's skeleton to the 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
|
|
|
|
A skinned model's skeleton handed to physics (see <<physics,Physics>>, Ragdolls). A ragdoll is addressed by the node given to `ragdollNew`, usually a model instance's root, and a node without a ragdoll terminates the script in every call after `ragdollNew`. Bones are named by their joint nodes, the names `nodeFind` sees, compared without regard to case; distances are world units, angles degrees, impulses newton seconds and motor strength newton meters. Up to eight ragdolls at once, of up to 48 bones each.
|
|
|
|
[#ragdollnew]
|
|
==== ragdollNew
|
|
|
|
[source,text]
|
|
----
|
|
ragdollNew(node)
|
|
----
|
|
|
|
Looks at the node and under it for the first skinned mesh and builds a ragdoll recipe from its skeleton: every joint that has a child joint becomes a bone, a capsule running from the joint toward its children, jointed to the nearest bone above it by a swing-twist joint limited to 45 degrees of swing and 30 of twist. Joints with no child joint, such as fingertips and the tip of a tail, ride along with their parent bone and cannot be named in the other calls. Nothing is simulated yet: the model keeps animating until `ragdollActivate`. Calling it again on the same node discards the old ragdoll first. Raises an error when physics is unavailable, when no skinned mesh with bones is under the node, or when eight ragdolls already exist.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollactivate,ragdollActivate>>, <<ragdollsetjoint,ragdollSetJoint>>, <<modelinstance,modelInstance>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A guard who runs at the player; the ragdoll is ready before the tackle.
|
|
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) -- Recipe only; still running
|
|
----
|
|
|
|
[#ragdolldelete]
|
|
==== ragdollDelete
|
|
|
|
[source,text]
|
|
----
|
|
ragdollDelete(node)
|
|
----
|
|
|
|
Removes the ragdoll: its bodies and joints if it is active, then the recipe. The joint nodes stay where physics left them, so the model lies as it fell until an animation moves it, and the node is free for a new `ragdollNew`. A node without a ragdoll terminates the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollnew,ragdollNew>>, <<ragdolldeactivate,ragdollDeactivate>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Example
|
|
[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) -- Still for five seconds: clean it up
|
|
nodeDelete(body.node)
|
|
table.remove(fallen, i)
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#ragdollsetjoint]
|
|
==== ragdollSetJoint
|
|
|
|
[source,text]
|
|
----
|
|
ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
|
|
----
|
|
|
|
Tunes one bone's recipe. The values are read when the bodies are made, so call it before `ragdollActivate`; a ragdoll that is already active keeps its bodies until it is deactivated and activated again. The radius `0` asks for the automatic capsule, 0.22 of the bone's length and never thinner than `0.02` units. The swing is the half-angle of the cone the bone may move through relative to its parent, the twist how far it may turn about its own axis; both are clamped to `0` through `179`. Only bones can be named, not the leaf joints; the name is matched without regard to case, and a name with no bone raises an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the ragdoll's node.
|
|
* `jointName` -- the joint node's name, as in the model.
|
|
* `radius` -- the capsule's radius in world units, or `0` for automatic.
|
|
* `swingDegrees` -- how far the bone may swing from its parent; default `45`.
|
|
* `twistDegrees` -- how far it may twist about its own axis; default `30`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollnew,ragdollNew>>, <<ragdollactivate,ragdollActivate>>, <<nodefind,nodeFind>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
ragdollNew(fox)
|
|
ragdollSetJoint(fox, "b_Neck_04", 0, 30, 20) -- A stiffer neck
|
|
ragdollSetJoint(fox, "b_Tail01_012", 0.03, 80, 60) -- A whippy tail
|
|
for _, leg in ipairs({ "b_LeftLeg01_015", "b_RightLeg01_019" }) do
|
|
ragdollSetJoint(fox, leg, 0, 60, 15) -- Hips swing wide, barely twist
|
|
end
|
|
ragdollActivate(fox)
|
|
----
|
|
|
|
[#ragdollactivate]
|
|
==== ragdollActivate
|
|
|
|
[source,text]
|
|
----
|
|
ragdollActivate(node)
|
|
----
|
|
|
|
Hands the skeleton to physics from its current pose: every animation layer on the node stops, a dynamic capsule is made for each bone where its joint stands right now, the swing-twist joints are made between them, and from then on each physics step drives the joint nodes from the bodies, so the skinned mesh follows. The bones collide with the world and with each other, except each bone with its parent. The pose at this moment is what `ragdollSetStrength` pulls toward. A ragdoll that is already active is left as it is. Raises an error when a bone's body cannot be made.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdolldeactivate,ragdollDeactivate>>, <<ragdollapplyimpulse,ragdollApplyImpulse>>, <<ragdollsetstrength,ragdollSetStrength>>
|
|
|
|
.Example
|
|
[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) -- The controller lets go
|
|
ragdollActivate(guardFox) -- Physics takes the skeleton from here
|
|
ragdollApplyImpulse(guardFox, "b_Spine02_03", 40, 30, 0)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#ragdolldeactivate]
|
|
==== ragdollDeactivate
|
|
|
|
[source,text]
|
|
----
|
|
ragdollDeactivate(node)
|
|
----
|
|
|
|
Removes the bodies and joints and hands the skeleton back. The joint nodes keep the pose the ragdoll left them in, and nothing moves them until an animation plays; an `animationPlay` with a fade blends up from that pose, so a downed character can get to its feet. The recipe stays, so `ragdollActivate` can take over again later from wherever the animation has moved the skeleton. Does nothing to a ragdoll that is not active.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollactivate,ragdollActivate>>, <<animationplay,animationPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Once the body has settled, the hero gets up over half a second.
|
|
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)
|
|
----
|
|
|
|
Powers every joint with a motor pulling toward the pose the ragdoll was activated in, limited to that many newton meters; `0` (default) turns the motors off and the body goes limp. The motors are springs at 4 Hz, critically damped. It can be set before activation or while the ragdoll is moving, and takes effect at the next step. It does not stand a body up, since nothing pushes off the floor: a few tens of newton meters turn a fall into a stagger and let a downed character pull its limbs in.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Knocked down limp; a moment later the fighter tenses and curls up.
|
|
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)
|
|
----
|
|
|
|
Adds an impulse in newton seconds, along the world axes, to the body of the bone that starts at the named joint: the punch, the shot, the explosion. The ragdoll must be active, so call `ragdollActivate` first and then shove. Returns `false`, and does nothing, when the ragdoll is not active or when no bone has that name (leaf joints are not bones); the name is matched without regard to case.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the ragdoll's node.
|
|
* `jointName` -- the joint node's name, as in the model.
|
|
* `x`, `y`, `z` -- the impulse in newton seconds, world axes.
|
|
|
|
*Returns:* True when the bone was found and the ragdoll was active.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollactivate,ragdollActivate>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<nodefind,nodeFind>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A hit from the shooter's position: the impulse points away from the gun.
|
|
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)
|
|
----
|
|
|
|
Whether physics is driving the skeleton right now: `true` from `ragdollActivate` until `ragdollDeactivate` or `ragdollDelete`. A node without a ragdoll terminates the script.
|
|
|
|
*Returns:* True or false.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The health bar hangs over the model only while it is on its feet.
|
|
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)
|
|
----
|
|
|
|
Whether every bone's body has gone to sleep, which physics does once a body has been still for a moment. `false` while the ragdoll is not active, so it cannot be used to tell an idle model from a settled one; check `ragdollIsActive` too.
|
|
|
|
*Returns:* True or false.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ragdollisactive,ragdollIsActive>>, <<ragdolldeactivate,ragdollDeactivate>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The round ends once the loser has stopped moving.
|
|
function onOverlayUpdate()
|
|
if roundOver and ragdollIsResting(loser) then
|
|
restingFrames = restingFrames + 1
|
|
if restingFrames == 90 then
|
|
showResults()
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#scene]
|
|
[#save]
|
|
=== Save
|
|
|
|
Anything the game should still know next time it starts: high scores, the
|
|
player's options, how far they got. One table per game, kept as JSON in the
|
|
game's own data directory (see <<singegetdatapath,`singeGetDataPath`>>), so a
|
|
packed `.game` stays read only and two games never tread on each other.
|
|
|
|
Keys are yours. Values may be numbers, strings, booleans, or tables of those,
|
|
nested as deep as you like -- a whole high score table goes in as one value. A
|
|
function, or a handle to something the engine owns, does not: neither means
|
|
anything on the next run, and saving one is an error rather than a surprise
|
|
later.
|
|
|
|
**What you set is what is saved, and what you get back is yours.** Tables are
|
|
copied on the way in and on the way out, so changing a table you read back
|
|
changes nothing until you save it again, and changing one you saved does not
|
|
reach into the save afterwards. Keeping a preview the player then cancels is
|
|
therefore just a matter of not saving it.
|
|
|
|
Writing happens once at the end of any frame that changed something, however
|
|
many keys you set in it, and again when the game shuts down -- including
|
|
anything set inside <<onshutdown,`onShutdown`>>. For a cabinet that may lose
|
|
power at any moment, <<saveflush,`saveFlush`>> writes immediately. The file is
|
|
written beside the old one and renamed over it, so losing power during a write
|
|
leaves the previous save rather than half of a new one.
|
|
|
|
Whole numbers come back as whole numbers. JSON has only one kind of number and
|
|
Lua has two, so a score saved as `100` would otherwise return as `100.0` and
|
|
print with a decimal point.
|
|
|
|
==== saveClear
|
|
|
|
[source,text]
|
|
----
|
|
saveClear()
|
|
----
|
|
|
|
Forgets everything and writes the empty save out. For a service menu's "reset high scores".
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== saveDelete
|
|
|
|
[source,text]
|
|
----
|
|
saveDelete(key)
|
|
----
|
|
|
|
Removes one key. Deleting a key that was never there is not an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `key` -- the key to remove.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== saveFlush
|
|
|
|
[source,text]
|
|
----
|
|
saveFlush()
|
|
----
|
|
|
|
Writes the save out now rather than at the end of the frame. Use it after something you would hate to lose -- a new high score on a cabinet that is switched off at the wall.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<saveset,saveSet>>
|
|
|
|
==== saveGet
|
|
|
|
[source,text]
|
|
----
|
|
value = saveGet(key [, default])
|
|
----
|
|
|
|
What was saved under that key. When nothing was, the default comes back, or `nil` when there is no default -- which is how a first run is told apart from a later one.
|
|
|
|
*Parameters:*
|
|
|
|
* `key` -- the key to read.
|
|
* `default` -- optional; what to answer when the key is not there.
|
|
|
|
*Returns:* the saved value, the default, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<saveset,saveSet>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Count the runs, and remember the best score.
|
|
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
|
|
|
|
[source,text]
|
|
----
|
|
everything = saveGetAll()
|
|
----
|
|
|
|
A copy of the whole save, for reading: dumping it while you are debugging, or walking keys whose names the game does not know in advance. Changing what comes back changes nothing -- <<savesetall,`saveSetAll`>> is what writes a whole table.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* a table; empty when nothing has been saved.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<savesetall,saveSetAll>>, <<saveget,saveGet>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- What is in there? inspect is bundled for exactly this.
|
|
debugPrint(require("inspect")(saveGetAll()))
|
|
----
|
|
|
|
==== saveSet
|
|
|
|
[source,text]
|
|
----
|
|
saveSet(key, value)
|
|
----
|
|
|
|
Keeps a value until the game is uninstalled. The write happens at the end of the frame, so setting twenty keys in a loop writes one file.
|
|
|
|
*Parameters:*
|
|
|
|
* `key` -- the key to write.
|
|
* `value` -- a number, a string, a boolean, or a table of those.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<saveget,saveGet>>, <<saveflush,saveFlush>>
|
|
|
|
==== saveSetAll
|
|
|
|
[source,text]
|
|
----
|
|
saveSetAll(everything)
|
|
----
|
|
|
|
Replaces the whole save with this table, in one step. Keys that were there and are not in the table you give are gone.
|
|
|
|
Moving an old save to a new layout is why it exists. Doing it with <<saveclear,`saveClear`>> and a loop of <<saveset,`saveSet`>> leaves a moment where the save is empty, and a machine switched off in that moment loses everything; this has no such moment, because the file is written once at the end of the frame from the finished table.
|
|
|
|
*Parameters:*
|
|
|
|
* `everything` -- table; what the save should now contain. The same values `saveSet` takes.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<savegetall,saveGetAll>>, <<saveclear,saveClear>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Version 1 kept one score in two keys; version 2 keeps a table of them.
|
|
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
|
|
|
|
The scene is the 3D layer drawn between the disc video and the overlay. It is off until `sceneEnable(true)`, is sized like the overlay, and clears to `sceneSetBackground` every frame. The `sceneSet*` calls set the whole layer's look: ambient light, sky, fog, exposure and tone curve, bloom, antialiasing and shadow quality; colors are integers from `0` to `255` and distances are world units. `sceneProject` and `sceneUnproject` bridge world space and overlay coordinates, and `sceneGetStats` reports last frame's work. See <<scenes3d,3D Scenes>>.
|
|
|
|
[#sceneenable]
|
|
==== sceneEnable
|
|
|
|
[source,text]
|
|
----
|
|
sceneEnable(enabled)
|
|
----
|
|
|
|
Turns the 3D layer on or off. Nodes, meshes and materials can be built before it is on and survive it being turned off; only drawing stops. Raises an error when turning it on with no usable GPU (see <<scenes3d,3D Scenes>>), so wrap the first call in `pcall` if the game has a 2D fallback. The display refreshes on the next frame either way.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetbackground,sceneSetBackground>>, <<scenesetantialias,sceneSetAntialias>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fall back to the plain overlay when 3D is unavailable.
|
|
has3d = pcall(sceneEnable, true)
|
|
if has3d then
|
|
buildArena()
|
|
else
|
|
debugPrint("No 3D here; using the 2D attract mode.")
|
|
end
|
|
|
|
function onPause()
|
|
sceneEnable(false)
|
|
end
|
|
----
|
|
|
|
[#scenegetsize]
|
|
==== sceneGetSize
|
|
|
|
[source,text]
|
|
----
|
|
width, height = sceneGetSize()
|
|
----
|
|
|
|
The layer's size, which is the overlay's resolution: the space `sceneProject` and `sceneUnproject` work in, and the size the scene is rendered at. Change it with `overlaySetResolution`. Before the layer has been sized both values are `0`.
|
|
|
|
*Returns:* Two integers, the width and height in overlay coordinates.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<overlaysetresolution,overlaySetResolution>>, <<sceneunproject,sceneUnproject>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A crosshair at the center of the view, and the thing under it.
|
|
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()
|
|
----
|
|
|
|
Last frame's numbers from the window's camera: meshes collected for drawing, how many of them were inside the camera's view, the draw calls they became after instancing, and the texture memory the scene holds in kilobytes. For tuning: a scene with hundreds of `drawn` but a handful of `calls` is sharing its meshes well, and a `textureKB` that will not fit a Raspberry Pi's memory wants KTX2 textures. Rendered views do not count.
|
|
|
|
*Returns:* Four integers.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneenable,sceneEnable>>, <<modelinstance,modelInstance>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A debug readout in the corner while tuning the level.
|
|
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
|
|
----
|
|
|
|
[#sceneprobegui]
|
|
==== sceneProbeGui
|
|
|
|
[source,text]
|
|
----
|
|
gui, x, y = sceneProbeGui(sx, sy)
|
|
----
|
|
|
|
Which GUI is on the surface under an overlay point, and where on it: a ray from the camera through the point, using the camera of the last rendered frame, against the triangles of every visible node whose material shows a GUI (`materialSetGui`), taking the nearest hit's texture coordinates as the place on the GUI. Returns the GUI handle and the pixel in that GUI's own coordinates (`0, 0` at its top left, `guiGetWidth`, `guiGetHeight` at its bottom right), or `nil` alone when the point is off every such surface, the scene is off, or nothing has rendered yet. The engine asks the same question when the mouse or a light gun points at the scene while a GUI that takes input is on a surface and nothing drawn flat is under the pointer, so clicks on a monitor in the scene need no script; the call is for a game that wants the answer itself, for a laser sight, a hover effect or a test.
|
|
|
|
*Parameters:*
|
|
|
|
* `sx, sy` -- the point, in overlay coordinates (a mouse or light gun position).
|
|
|
|
*Returns:* An integer and two integers, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<materialsetgui,materialSetGui>>, <<guisetinput,guiSetInput>>, <<sceneproject,sceneProject>>, <<sceneunproject,sceneUnproject>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A light gun's dot glows where it lands on the cockpit's screen.
|
|
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)
|
|
----
|
|
|
|
Where a world point falls in overlay coordinates, using the camera of the last rendered frame. `depth` is `0` at the near plane and `1` at the far plane, for sorting labels or fading them with distance. `inFront` is `false` when the point is behind the camera, and `x` and `y` are then meaningless; before the first frame has rendered there is nothing to project against, and every value comes back `0` with `inFront` `false`. Points in front but outside the view come back with coordinates off the overlay's edges, so check them against `sceneGetSize` before drawing. The usual use is a name tag or health bar drawn with `fontPrint` or `spriteDraw` above a 3D object.
|
|
|
|
*Parameters:*
|
|
|
|
* `wx, wy, wz` -- the world point, in world units.
|
|
|
|
*Returns:* Two numbers, a number, and a boolean.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneunproject,sceneUnproject>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<scenegetsize,sceneGetSize>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A name and health bar floating above each character.
|
|
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)
|
|
----
|
|
|
|
The world point `distance` units along the ray through an overlay pixel, using the camera of the last rendered frame. Two distances give a ray for picking with `physicsRaycast`; one places an object "where the player clicked". Before the first frame has been drawn it returns the origin.
|
|
|
|
*Parameters:*
|
|
|
|
* `sx, sy` -- the overlay pixel, in overlay coordinates.
|
|
* `distance` -- how far along the ray, in world units; `0` is the near plane.
|
|
|
|
*Returns:* Three numbers, the world point.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneproject,sceneProject>>, <<mousegetposition,mouseGetPosition>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Drop a marker on whatever the player clicked, or 8 units out if nothing.
|
|
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)
|
|
----
|
|
|
|
Light from everywhere, `0` to `255` per channel, treated as sRGB; the default is a dim gray of `26, 26, 26`. It lifts the sides that no light reaches so they are not black, and `materialSetOcclusionMap` darkens it in crevices. When a sky is set and `sceneSetEnvironment` is on, the sky's light replaces this value.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightnew,lightNew>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetsky,sceneSetSky>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A moonlit yard: a faint blue everywhere, one cold directional moon.
|
|
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)
|
|
----
|
|
|
|
Turns 4x multisampling on or off; it is on by default wherever the GPU offers it for the layer's formats, and silently absent where it does not. Turning it off buys speed on a Raspberry Pi. The render targets are rebuilt at once, so call it during setup rather than every frame.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneenable,sceneEnable>>, <<scenesetshadowsize,sceneSetShadowSize>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A quality setting from the game's options screen.
|
|
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)
|
|
----
|
|
|
|
The color the layer clears to each frame, `0` to `255` per channel. `a` is `255` when omitted, an opaque backdrop; below `255` lets the video show through, and `0` (the usual choice for a game with a disc) makes the background fully transparent. Until it is called the layer clears to black and transparent. A sky from `sceneSetSky` is drawn over the background, so the video no longer shows through while one is set.
|
|
|
|
*Parameters:*
|
|
|
|
* `r, g, b` -- the clear color.
|
|
* `a` -- the clear alpha; `255` when omitted.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<sceneenable,sceneEnable>>, <<scenesetsky,sceneSetSky>>, <<scenesetfog,sceneSetFog>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The disc shows behind the 3D cockpit; a solid night sky for the menu.
|
|
function showCockpit()
|
|
sceneSetBackground(0, 0, 0, 0)
|
|
discPlay()
|
|
end
|
|
|
|
function showMenu()
|
|
discPause()
|
|
sceneSetBackground(10, 12, 30)
|
|
end
|
|
----
|
|
|
|
[#scenesetbloom]
|
|
==== sceneSetBloom
|
|
|
|
[source,text]
|
|
----
|
|
sceneSetBloom(threshold, strength)
|
|
----
|
|
|
|
A glow around everything brighter than `threshold`, taken from the scene's linear light before exposure and the tone curve (where `1` is white), and added back at `strength`. The defaults are a threshold of `1` and a strength of `0`, no bloom; `0.2` to `0.4` is subtle, `1` is a haze. The glow is blurred over a chain of half-size passes, so it costs little, and only the window's camera gets it: rendered views skip it. Negative values are clamped to `0`.
|
|
|
|
*Parameters:*
|
|
|
|
* `threshold` -- the linear brightness above which a pixel glows; `1` is white before the tone curve.
|
|
* `strength` -- how much of the glow is added back; `0` for none.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetexposure,sceneSetExposure>>, <<materialsetemissive,materialSetEmissive>>, <<scenesettonemap,sceneSetTonemap>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Neon and headlights glow; the pavement does not.
|
|
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)
|
|
----
|
|
|
|
Whether the sky set by `sceneSetSky` lights the scene: its diffuse light from every direction in place of `sceneSetAmbient`, and its reflections on metals and glossy surfaces, sharp on a mirror and blurred as roughness rises. On by default, so a sky lights the scene as soon as it is set; `false` keeps the sky as a backdrop only and the flat ambient returns. It has no effect without a sky.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetsky,sceneSetSky>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A painted backdrop that must not tint the chrome.
|
|
sceneSetSky(DIR .. "Skies/matteBackdrop.png")
|
|
sceneSetEnvironment(false)
|
|
sceneSetAmbient(40, 40, 45)
|
|
----
|
|
|
|
[#scenesetexposure]
|
|
==== sceneSetExposure
|
|
|
|
[source,text]
|
|
----
|
|
sceneSetExposure(stops)
|
|
----
|
|
|
|
Brightens or darkens the whole scene in photographic stops, `-10` to `10` (values outside are clamped): `+1` doubles the light, `-1` halves it. The default is `0`. It is applied after bloom and before the tone curve, so a bright scene at `+2` compresses toward white rather than clipping, and a cave and a beach can share the same lights. Animate it for a slow adjustment when walking from indoors to daylight.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesettonemap,sceneSetTonemap>>, <<lightsetintensity,lightSetIntensity>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Eyes adjusting: ease the exposure toward the area's target each frame.
|
|
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()
|
|
----
|
|
|
|
Distance fog: clear up to `near` units from the camera, fully the fog color at `far` and beyond, on meshes (unlit ones too) and particles alike; additive particles fade out instead of going gray. With no arguments, or a `far` no greater than `near`, there is no fog. The sky is not fogged, so pick a color close to its horizon, or to the background when there is no sky. Any other number of arguments raises an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `r, g, b` -- the fog color, `0` to `255`.
|
|
* `near` -- the distance where fog begins, in world units from the camera.
|
|
* `far` -- the distance where nothing but fog remains.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetsky,sceneSetSky>>, <<scenesetbackground,sceneSetBackground>>, <<camerasetperspective,cameraSetPerspective>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Morning haze over the hills that burns off as the level goes on.
|
|
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)
|
|
----
|
|
|
|
How many maps a directional light's shadow is split into along the view, `1` to `4` (values outside are clamped; the default is `3`). Each cascade covers a slice of the camera's view out to `sceneSetShadowDistance`, the nearest slice the smallest, so shadows near the camera are sharp however large the level. `1` fits one map to the whole scene, which is right for small scenes, and is always used with an orthographic camera. Each cascade is one more shadow pass for every casting directional light; `2` is a fair choice on a Raspberry Pi.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetshadowdistance,sceneSetShadowDistance>>, <<scenesetshadowsize,sceneSetShadowSize>>, <<lightsetshadow,lightSetShadow>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A large outdoor level: sharp shadows near the player, reaching far.
|
|
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)
|
|
----
|
|
|
|
How far from the camera cascaded shadows reach, in world units; the default is `60` and values below `1` are clamped to `1`. Beyond it a directional light casts no shadow. Shorter distances make every cascade sharper, since the same maps cover less ground, so set it to about as far as the player can make out a shadow. It only matters with more than one cascade under a perspective camera.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetshadowsize,sceneSetShadowSize>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Shadows only need to reach the far wall of the arena.
|
|
sceneSetShadowCascades(2)
|
|
sceneSetShadowDistance(35)
|
|
sun = lightNew(LIGHT_DIRECTIONAL)
|
|
nodeSetPosition(sun, 10, 20, 8)
|
|
nodeLookAt(sun, 0, 0, 0)
|
|
lightSetShadow(sun, true)
|
|
----
|
|
|
|
[#scenesetshadowsize]
|
|
==== sceneSetShadowSize
|
|
|
|
[source,text]
|
|
----
|
|
sceneSetShadowSize(size)
|
|
----
|
|
|
|
Texels per side of every shadow map, cascade and cube face, `256` to `4096` (values outside are clamped; the default is `1024`). Larger is sharper and slower, in memory as well as time, since every casting light has its own maps at this size. `2048` suits a desktop, `512` a Raspberry Pi. The maps are rebuilt on the next frame.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<lightsetshadow,lightSetShadow>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetantialias,sceneSetAntialias>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Size the shadow maps to the machine.
|
|
if singeGetPlatform() == "raspberrypi" then
|
|
sceneSetShadowSize(512)
|
|
else
|
|
sceneSetShadowSize(2048)
|
|
end
|
|
----
|
|
|
|
[#scenesetsky]
|
|
==== sceneSetSky
|
|
|
|
[source,text]
|
|
----
|
|
sceneSetSky(file)
|
|
sceneSetSky(nil)
|
|
sceneSetSky()
|
|
----
|
|
|
|
Wraps the scene in an equirectangular panorama: a Radiance `.hdr` (as HDRI sites ship, keeping its range), or a PNG, JPEG or other image SDL_image reads, decoded from sRGB. The name is game relative like `spriteLoad`. The sky is drawn behind everything in place of the background color (so the video no longer shows through) and lights the scene unless `sceneSetEnvironment(false)`. Its center faces -Z and its top is up. It is resampled into a cube of up to 1024 texels a face, so a 2048 by 1024 image is plenty and larger ones take a moment to prepare on a Raspberry Pi. With no argument, or `nil`, the sky is removed. Raises an error when the file cannot be read or 3D is unavailable.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetskyintensity,sceneSetSkyIntensity>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetfog,sceneSetFog>>, <<scenesetbackground,sceneSetBackground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- An overcast sky for the docks level, with fog matched to its horizon.
|
|
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)
|
|
----
|
|
|
|
Brightness of the sky and of the light it casts, `1` as loaded; negative values are clamped to `0`. Useful to dim a PNG sky that was too bright, to make an `.hdr` sky the scene's main light, or to fade the sky in and out. It multiplies the sky itself, unlike `sceneSetExposure`, which scales everything.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetsky,sceneSetSky>>, <<scenesetexposure,sceneSetExposure>>, <<scenesetenvironment,sceneSetEnvironment>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Dusk: the sky and its light dim over ten seconds.
|
|
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)
|
|
----
|
|
|
|
The curve that maps the scene's linear light to the screen, applied after exposure. `TONEMAP_NEUTRAL` (the default) keeps colors faithful and compresses only the brightest light; `TONEMAP_ACES` is the filmic curve with more contrast and a warmer roll-off; `TONEMAP_NONE` clips at white. Video materials, unlit surfaces and the sky pass through the same curve, so a game whose disc must look exactly as filmed wants `TONEMAP_NONE` with an exposure of `0`. Any other value raises an error.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<scenesetexposure,sceneSetExposure>>, <<scenesetbloom,sceneSetBloom>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A filmic look for the cutscene, faithful color for play.
|
|
function onCutsceneStart()
|
|
sceneSetTonemap(TONEMAP_ACES)
|
|
sceneSetExposure(0.5)
|
|
end
|
|
|
|
function onCutsceneEnd()
|
|
sceneSetTonemap(TONEMAP_NEUTRAL)
|
|
sceneSetExposure(0)
|
|
end
|
|
----
|
|
|
|
[#script]
|
|
=== Script
|
|
|
|
WARNING: These functions let a Singe script hand control to another Singe script: the mechanism behind the built-in `Menu.singe` launcher. Game developers should not reach for these; build your game as a single script and let the menu system handle chaining. They are documented here for completeness and for anyone maintaining the menu itself.
|
|
|
|
Both calls take a table shaped like a `GAMES[]` entry in `games.dat` and queue it for the engine's script runner. The new configuration starts from the running one, so command line options such as the data directory and tracing carry over, but the disc, video and container never do: each entry brings its own. The current script ends at the end of the frame, `onShutdown` fires, and the queued script starts with a fresh Lua state (see <<enginecallbacks,Engine Callbacks>>). The new script's data directory follows the same rule as a command line launch (see `singeGetDataPath`); when it cannot be created the calling script is aborted.
|
|
|
|
[#scriptexecute]
|
|
==== scriptExecute
|
|
|
|
[source,text]
|
|
----
|
|
scriptExecute(config)
|
|
----
|
|
|
|
Replaces the running script with the one the table describes. The current script stops at the end of this frame and `onShutdown` runs; control never comes back to it, and when the new script quits the engine moves on to whatever else is queued or exits. String keys the engine knows are `SCRIPT`, `CONTAINER`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`, `LEGACY_SPRITE_ARGS`, `CANVAS_X` and `CANVAS_Y`; other keys are ignored and non-string keys are skipped. A key of the wrong type ends the program; a non-table argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `config` -- a table of `games.dat` fields, at minimum `SCRIPT` or `CONTAINER`.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<scriptpush,scriptPush>>, <<singequit,singeQuit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A launcher that never returns to itself.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_START1 then
|
|
scriptExecute({
|
|
SCRIPT = DIR .. "../Lair/Lair.singe",
|
|
VIDEO = DIR .. "../Lair/Lair.mp4",
|
|
})
|
|
end
|
|
end
|
|
----
|
|
|
|
[#scriptpush]
|
|
==== scriptPush
|
|
|
|
[source,text]
|
|
----
|
|
scriptPush(config)
|
|
----
|
|
|
|
Starts the script the table describes and queues the current configuration behind it, so when the new script quits the current script runs again from the top. Nothing is kept in place: the current script's `onShutdown` runs now, its Lua state is discarded, and the later run is a fresh start, which is why `Menu.singe` saves its selection to a file before pushing. The table takes the same keys as `scriptExecute`.
|
|
|
|
*Parameters:*
|
|
|
|
* `config` -- a table of `games.dat` fields, as for `scriptExecute`.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<scriptexecute,scriptExecute>>, <<singequit,singeQuit>>, <<onshutdown,onShutdown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: start the selected game and come back afterward.
|
|
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
|
|
|
|
Functions in the `singe*` namespace control the engine itself: the window, screenshots, the pause system, quitting and reloading, the audio delay used for lip sync, and a few paths and flags that come from the command line and `games.dat`. Nothing here takes a handle. Pause behavior is described in <<pausing,Pausing>> and the audio delay values in <<audiosync,Audio Sync>>.
|
|
|
|
[#singedisablepausekey]
|
|
==== singeDisablePauseKey
|
|
|
|
[source,text]
|
|
----
|
|
singeDisablePauseKey()
|
|
----
|
|
|
|
Gives the pause key to the script, exactly as `singeSetPauseKeyEnabled(false)` does. It is a function defined in `Framework.singe`, kept so older games run unchanged.
|
|
|
|
*Notes:* Legacy alias.
|
|
|
|
WARNING: New code should call <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> with `false`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singeenablepausekey,singeEnablePauseKey>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Older game code; the modern spelling is singeSetPauseKeyEnabled(false).
|
|
singeDisablePauseKey()
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_PAUSE then
|
|
togglePauseMenu()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singeenablepausekey]
|
|
==== singeEnablePauseKey
|
|
|
|
[source,text]
|
|
----
|
|
singeEnablePauseKey()
|
|
----
|
|
|
|
Returns the pause key to the engine, exactly as `singeSetPauseKeyEnabled(true)` does. It is a function defined in `Framework.singe`, kept so older games run unchanged.
|
|
|
|
*Notes:* Legacy alias.
|
|
|
|
WARNING: New code should call <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> with `true`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singedisablepausekey,singeDisablePauseKey>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Older game code; the modern spelling is singeSetPauseKeyEnabled(true).
|
|
function pauseMenuClose()
|
|
singeSetPauseFlag(false)
|
|
singeEnablePauseKey()
|
|
end
|
|
----
|
|
|
|
[#utilsha256]
|
|
==== utilSha256
|
|
|
|
[source,text]
|
|
----
|
|
digest = utilSha256(data)
|
|
----
|
|
|
|
SHA-256 of a string, as 64 lowercase hex characters. Binary-safe, so it works on the bytes of a file
|
|
as readily as on text.
|
|
|
|
For deciding whether the bytes you have are the bytes you were promised: a downloaded game against
|
|
the digest its catalogue entry carries, or a server's public key against a pin. The bundled `md5`
|
|
module is still there for a checksum, but a digest used to decide whether to *trust* something has
|
|
to be one that is still worth trusting.
|
|
|
|
*Parameters:*
|
|
|
|
* `data` -- a string of bytes. Empty aborts the script.
|
|
|
|
*Returns:* string, 64 lowercase hex characters.
|
|
|
|
*Since:* 3.00.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Refuse a download that did not arrive intact.
|
|
local file = io.open("game.part", "rb")
|
|
local bytes = file:read("*a")
|
|
file:close()
|
|
if utilSha256(bytes) ~= expected then
|
|
os.remove("game.part")
|
|
end
|
|
----
|
|
|
|
[#singegetaudiocalibration]
|
|
==== singeGetAudioCalibration
|
|
|
|
[source,text]
|
|
----
|
|
milliseconds = singeGetAudioCalibration()
|
|
----
|
|
|
|
Returns the per-machine audio delay in effect, in milliseconds: the value the menu's calibration screen saved to `machine.cfg` in the data root, or one set since by `singeSetAudioCalibration`. Zero when the machine has never been calibrated. It applies to every game on the machine, on top of the per-game delay from `singeGetAudioDelay`.
|
|
|
|
*Returns:* integer milliseconds.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singesetaudiocalibration,singeSetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>, <<singegetaudiolatency,singeGetAudioLatency>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: start the calibration screen from the saved value.
|
|
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()
|
|
----
|
|
|
|
Returns the per-game audio delay in effect, in milliseconds, whether it came from `--audiodelay`, the `AUDIO_DELAY` key of the game's `games.dat` entry, or `singeSetAudioDelay`. Zero when none has been set. It is added to the machine calibration and the measured device latency when the engine decides when to show each video frame.
|
|
|
|
*Returns:* integer milliseconds.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singesetaudiodelay,singeSetAudioDelay>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Service menu: show the current sync offset next to its slider.
|
|
function drawSyncSetting(y)
|
|
local delay = singeGetAudioDelay()
|
|
fontPrint(40, y, string.format("A/V offset: %+d ms", delay))
|
|
end
|
|
----
|
|
|
|
[#singegetaudiolatency]
|
|
==== singeGetAudioLatency
|
|
|
|
[source,text]
|
|
----
|
|
milliseconds = singeGetAudioLatency()
|
|
----
|
|
|
|
Returns the audio device queue the engine measured when it opened the mixer, in milliseconds: the time between handing samples to the mixer and their reaching the device, before any calibration or per-game delay. The engine already compensates for it; a calibration screen needs it to schedule its click and flash, and it is worth printing in a diagnostic view.
|
|
|
|
*Returns:* integer milliseconds.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: the flash follows the click by the device queue plus the candidate.
|
|
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()
|
|
----
|
|
|
|
Returns the directory Singe writes to for this game, with a trailing path separator so a file name can be appended directly. With `--datadir`, or for a packed game, it is a subdirectory of that base named for the game (for a packed game, the database's name and then the script's directory inside it); without `--datadir` the base is `data` in the game directory. Screenshots, video indexes and `trace.txt` go there, and so should save games, high scores and settings; Singe 2.x wrote them beside the game, which may be read only.
|
|
|
|
*Returns:* string path ending in a separator.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<singegetscriptpath,singeGetScriptPath>>, <<singescreenshot,singeScreenshot>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe keeps its last selection between runs.
|
|
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()
|
|
----
|
|
|
|
The `GAME_ID` from this game's `games.dat` entry, or `nil` when it has none. It is the id the
|
|
online service knows the game by, and the key a leaderboard is kept under.
|
|
|
|
**The format is a UUID**, in the canonical 36-character form -- eight hex digits, three groups of
|
|
four, then twelve, separated by hyphens:
|
|
|
|
----
|
|
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
|
|
----
|
|
|
|
Nothing else is accepted. The id is a path segment in the service's URLs, so one containing a slash
|
|
or a question mark would publish and then never be reachable, and two ids differing only in case
|
|
would be two games to the database and one game to anybody reading them. Comparison is
|
|
case-insensitive and storage is lowercase, so an id typed in capitals is the same game.
|
|
|
|
Any UUID will do -- generate one however you like, or let `publish.lua` print one. A game without
|
|
one can be played but not ranked: there would be nothing stable to key a board on, and an id
|
|
invented at run time would make a different board every time the files moved.
|
|
|
|
*Returns:* string, or `nil`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<onlinescores,Online high scores>>, <<gamesdat,games.dat>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Only offer the leaderboard when this game has an identity to rank under.
|
|
if singeGetGameId() then
|
|
showLeaderboardButton()
|
|
end
|
|
----
|
|
|
|
[#singegetheight]
|
|
==== singeGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = singeGetHeight()
|
|
----
|
|
|
|
Returns the height of the Singe window in screen pixels, not overlay units, as the window is at the moment of the call. Use `overlayGetHeight` for drawing; this is for a settings screen that reports the display, or for deciding how much detail a scene can afford.
|
|
|
|
*Returns:* integer pixels.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<singegetwidth,singeGetWidth>>, <<overlaygetheight,overlayGetHeight>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Report the display in the service menu.
|
|
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()
|
|
----
|
|
|
|
Returns the engine's pause flag: `true` while the game is paused by the pause key or by `singeSetPauseFlag`. This is separate from whether the disc is on a paused frame (`discGetState`); read it to know whether a pause was deliberately requested. While the pause key holds the game frozen no callback runs, so from inside a callback the flag can only be seen as `true` after the script's own `singeSetPauseFlag(true)`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<singesetpauseflag,singeSetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Timers stand still while the game's own pause is up.
|
|
function onOverlayUpdate()
|
|
if not singeGetPauseFlag() then
|
|
elapsed = elapsed + 1
|
|
updateEnemies()
|
|
end
|
|
drawFrame()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#singegetscriptpath]
|
|
==== singeGetScriptPath
|
|
|
|
[source,text]
|
|
----
|
|
path = singeGetScriptPath()
|
|
----
|
|
|
|
Returns the path of the running script file as the engine resolved it. `Framework.singe` derives the global `DIR`, the script's directory with its trailing separator, from this value, and `DIR` is what most games use to build asset paths.
|
|
|
|
*Returns:* string path.
|
|
|
|
*Since:* 1.15 (RDG)
|
|
*See also:* <<singegetdatapath,singeGetDataPath>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Framework.singe derives DIR this way; games then load relative to it.
|
|
DIR = singeGetScriptPath():match("(.*[/\\])") or "./"
|
|
titleSprite = spriteLoad(DIR .. "images/title.png")
|
|
----
|
|
|
|
[#singegetsysteminfo]
|
|
==== singeGetSystemInfo
|
|
|
|
[source,text]
|
|
----
|
|
info = singeGetSystemInfo()
|
|
----
|
|
|
|
Everything the trace header knows about this machine and this run, in one
|
|
table. It is meant for a service screen and for bug reports: the answer to
|
|
"what was it running on" without asking the player to find a log.
|
|
|
|
*Returns:* a table with these fields, all strings except the four sizes:
|
|
|
|
* `version` -- the engine version, as `SINGE_VERSION_STRING`.
|
|
* `os` -- the operating system name and release, and the architecture.
|
|
* `cpu` -- the processor, its core count and the memory in megabytes.
|
|
* `renderer` -- the name of SDL's 2D renderer in use.
|
|
* `gpu` -- the SDL_GPU driver behind the 3D scene, or a note that 3D is
|
|
unavailable.
|
|
* `decoder` -- which hardware video decoders the build will try, in words.
|
|
* `audio` -- the audio formats compiled in, comma separated.
|
|
* `soundFont` -- the SoundFont MIDI playback is using, or why there is none.
|
|
* `midi` -- the MIDI ports open, or a note that none have been asked for.
|
|
* `dataPath` -- this game's data directory.
|
|
* `dataRoot` -- the data directory the engine was given, one above `dataPath`,
|
|
which every game on the machine writes under.
|
|
* `windowWidth`, `windowHeight` -- integers, the window in pixels.
|
|
* `canvasWidth`, `canvasHeight` -- integers, the canvas in pixels.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<singegetdatapath,singeGetDataPath>>, <<servicetools,The Service Tools>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A one-key diagnostic screen.
|
|
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()
|
|
----
|
|
|
|
Returns the wall clock in milliseconds since the engine started. It is the clock for timers, debounces and animation, and it keeps running while the game is frozen by the pause key, so a timer that must not count paused time should read `singeGetPauseFlag` or measure from the moment of thawing. Lua's `os.clock()` normally measures processor time, which stalls while the engine idles; Singe replaces it with wall seconds since the engine started, so existing games that used it as a wall clock keep working.
|
|
|
|
*Returns:* integer milliseconds.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singegetpauseflag,singeGetPauseFlag>>, <<debugprint,debugPrint>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Blink the "insert coin" prompt twice a second.
|
|
function drawAttractPrompt()
|
|
local phase = (singeGetTicks() // 250) % 2
|
|
if phase == 0 then
|
|
fontPrint(120, 200, "INSERT COIN")
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singegetwidth]
|
|
==== singeGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = singeGetWidth()
|
|
----
|
|
|
|
Returns the width of the Singe window in screen pixels, the counterpart of `singeGetHeight`. Overlay drawing uses `overlayGetWidth` instead.
|
|
|
|
*Returns:* integer pixels.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<singegetheight,singeGetHeight>>, <<overlaygetwidth,overlayGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Pick a texture budget from the window size.
|
|
local pixels = singeGetWidth() * singeGetHeight()
|
|
if pixels > 1920 * 1080 then
|
|
textureSet = "high"
|
|
else
|
|
textureSet = "low"
|
|
end
|
|
----
|
|
|
|
[#singehasgpu]
|
|
==== singeHasGpu
|
|
|
|
[source,text]
|
|
----
|
|
present = singeHasGpu()
|
|
----
|
|
|
|
Whether the GPU device came up. Everything drawn through it is unavailable
|
|
without one: the 3D scene, 3D particles, and every GUI document. 2D
|
|
particles are drawn with the overlay and need no device. Ask this
|
|
before asking for any of them, because <<guinew,guiNew>> and the scene calls
|
|
end the script rather than answering `false` -- there is no way to try and
|
|
recover.
|
|
|
|
A machine can lack one because it has no Vulkan, Direct3D 12 or Metal driver
|
|
at all, or because `--nogpu` was given. The bundled menu calls this at startup
|
|
and picks a renderer from the answer (see <<menurenderers,The two renderers>>).
|
|
|
|
*Returns:* boolean, true when a GPU device is present.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<guinew,guiNew>>, <<singegetsysteminfo,singeGetSystemInfo>>, <<sceneperformance,Performance and Requirements>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A game with a 3D title screen and a flat one to fall back on.
|
|
if singeHasGpu() then
|
|
buildSceneTitle()
|
|
else
|
|
buildSpriteTitle()
|
|
end
|
|
----
|
|
|
|
[#singequit]
|
|
==== singeQuit
|
|
|
|
[source,text]
|
|
----
|
|
singeQuit()
|
|
----
|
|
|
|
Ends the running script at the end of this frame: the game loop stops, `onShutdown` runs, and the engine moves on to the next queued script or exits when there is none. A game launched from the menu with `scriptPush` therefore returns to the menu, which restarts from the top. In the threaded model returning from `singeMain` calls this for you. The switch mapped to `INPUT_QUIT` in `controls.cfg` and closing the window end the script the same way. Do not call it from `onShutdown`, which is already the exit path.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<onshutdown,onShutdown>>, <<singereload,singeReload>>, <<scriptpush,scriptPush>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_QUIT then
|
|
singeQuit()
|
|
elseif what == SWITCH_START1 and gameOver then
|
|
startGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singereload]
|
|
==== singeReload
|
|
|
|
[source,text]
|
|
----
|
|
singeReload()
|
|
----
|
|
|
|
Runs the game again from its script at the end of this frame, as `F5` and a changed file do under `--reload`; this call works without the option. Every sound stops, the Lua state is discarded without calling `onShutdown`, everything the script loaded is freed, the scene, physics (its debug drawing included), particles and navigation are reset, the overlay returns to its default resolution, and the sound effect volume, mouse mode, mouse capture, keyboard mode, pause key, sound listener and pause flag return to their defaults. Keys and buttons held through the reload are ignored until released, as at a fresh start, and the disc is parked on its first frame, paused, with the default audio track selected, as at startup. The engine itself (the window, the controllers, the mice) stays as it is. It suits a debug menu's restart item or a level editor reloading what it just saved.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<singequit,singeQuit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Editor: save the level, then restart the game on it.
|
|
function onKeyPressed(keysym, scancode)
|
|
if scancode == SCANCODE.F9.value then
|
|
saveLevel(singeGetDataPath() .. "level.lua")
|
|
singeReload()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singesavegeometry]
|
|
==== singeSaveGeometry
|
|
|
|
[source,text]
|
|
----
|
|
singeSaveGeometry()
|
|
----
|
|
|
|
Keeps the picture's current scale, shift and rotation for this machine. They
|
|
go into `machine.cfg` in the data root, beside the audio delay, and the engine
|
|
applies them to every game it starts afterwards, from the menu or from the
|
|
command line, and across `singeReload`. An option actually typed on the command
|
|
line wins: `--scalefactor` overrides the saved scale, `--shiftx` or `--shifty`
|
|
the saved shift, `--rotate` the saved rotation. The file is written whole, so
|
|
the audio delay is kept as it stands.
|
|
|
|
This is a cabinet setting, not a game setting: it is how a monitor is squared
|
|
up once rather than once per game. The Display tool in the bundled menu is
|
|
exactly this call behind button 1.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vldpsetscale,vldpSetScale>>, <<vldpsetshift,vldpSetShift>>, <<vldpsetrotate,vldpSetRotate>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The operator is happy with the picture: keep it for the cabinet.
|
|
function onInputPressed(input)
|
|
if input == SWITCH_BUTTON1 then
|
|
singeSaveGeometry()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singescreenshot]
|
|
==== singeScreenshot
|
|
|
|
[source,text]
|
|
----
|
|
singeScreenshot()
|
|
----
|
|
|
|
Requests a screenshot of the whole window, letterbox included, taken after the next frame is drawn and saved as a PNG in the game's data directory (see `singeGetDataPath`). Files are named `singe000.png` upward; each script run scans from zero for the first free name and later shots continue past the last one saved. The call forces a redraw so the shot is taken even while the display is idle. The switch mapped to `INPUT_SCREENSHOT` in `controls.cfg` does the same thing.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<singegetdatapath,singeGetDataPath>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A test script captures a few frames as it flies through the scene.
|
|
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)
|
|
----
|
|
|
|
Sets the per-machine audio delay, applies it at once, and writes it to `machine.cfg` in the data root so every game on the machine picks it up. The menu's calibration screen calls it; a game with its own service menu may too. The value is positive when the audio is heard later than the device reports; the engine delays video presentation by that much. Values outside `-1000` to `1000` abort the script. If `machine.cfg` cannot be written the value still applies for this run and a message goes to the console.
|
|
|
|
*Parameters:*
|
|
|
|
* `milliseconds` -- integer, `-1000` to `1000`, positive when the audio is late.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singesetaudiodelay,singeSetAudioDelay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: keep or discard the calibration the player dialed in.
|
|
function calibrationEnd(save)
|
|
if save then
|
|
singeSetAudioCalibration(CAL_VALUE)
|
|
end
|
|
CALIBRATING = false
|
|
end
|
|
----
|
|
|
|
[#singesetaudiodelay]
|
|
==== singeSetAudioDelay
|
|
|
|
[source,text]
|
|
----
|
|
singeSetAudioDelay(milliseconds)
|
|
----
|
|
|
|
Sets the per-game audio delay for this run: how much later (positive) or earlier (negative) the audio is heard than the engine can measure, so that video presentation shifts to match. It applies to the disc and to every video the script plays, on top of the machine calibration. Nothing is saved; a game offering the adjustment in a service menu stores the value with its other settings and calls this again at startup. Values outside `-1000` to `1000` abort the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `milliseconds` -- integer, `-1000` to `1000`, positive when the audio is late.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singegetaudiodelay,singeGetAudioDelay>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Service menu: nudge the offset and remember it.
|
|
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)
|
|
----
|
|
|
|
Sets the title of the Singe window. Call it once at startup with the game's display name; nothing else uses the value. The argument must be a string or a number.
|
|
|
|
*Parameters:*
|
|
|
|
* `title` -- the window title.
|
|
|
|
*Since:* 1.15 (RDG)
|
|
*See also:* <<singeversion,singeVersion>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
singeSetGameName("Space Ace")
|
|
keyboardSetMode(MODE_NORMAL)
|
|
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18)
|
|
----
|
|
|
|
[#singesetpauseflag]
|
|
==== singeSetPauseFlag
|
|
|
|
[source,text]
|
|
----
|
|
singeSetPauseFlag(paused)
|
|
----
|
|
|
|
Sets the engine's pause flag from the script. `true` pauses the disc if it is playing, every loaded video, and every sound effect channel, remembering which were running; `false` resumes exactly those. Unlike the pause key, this does not freeze the script: callbacks keep firing so the script can draw its pause menu and decide when to clear the flag. Games that manage their own pause should also take the pause key with `singeSetPauseKeyEnabled(false)`, because the engine's key toggles the same flag and would otherwise fight the script. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `paused` -- boolean.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<singegetpauseflag,singeGetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Chooses who owns the pause key mapped to `INPUT_PAUSE` in `controls.cfg`. While enabled (the default, restored on every reload) the engine owns it: pressing the key freezes the game completely (see <<pausing,Pausing>>), and in `MODE_NORMAL` the script never receives `SWITCH_PAUSE`. While disabled the engine ignores the key and `SWITCH_PAUSE` reaches `onInputPressed` and `onInputReleased` like any other switch, so the game can run its own pause with `singeSetPauseFlag`. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00
|
|
*See also:* <<singesetpauseflag,singeSetPauseFlag>>, <<singegetpauseflag,singeGetPauseFlag>>, <<singedisablepausekey,singeDisablePauseKey>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The engine may pause during play, but not while the player enters initials.
|
|
function beginInitials()
|
|
singeSetPauseKeyEnabled(false)
|
|
keyboardSetMode(MODE_FULL)
|
|
end
|
|
|
|
function endInitials()
|
|
keyboardSetMode(MODE_NORMAL)
|
|
singeSetPauseKeyEnabled(true)
|
|
end
|
|
----
|
|
|
|
[#singesetquitkeyenabled]
|
|
==== singeSetQuitKeyEnabled
|
|
|
|
[source,text]
|
|
----
|
|
singeSetQuitKeyEnabled(enabled)
|
|
----
|
|
|
|
Hypseus extension. Chooses whether the engine quits on the switch mapped to `INPUT_QUIT` in `controls.cfg`, from the keyboard or a controller. While enabled (the default, restored on every reload) pressing it ends the game. While disabled the engine ignores it, so a game can confirm or save first; the script receives `SWITCH_QUIT` through `onInputPressed` and `onInputReleased` either way, and `singeQuit` still ends the game. The window's close button always quits. `Framework.singe` defines `keyboardCatchQuit(catch)`, Hypseus Singe's name for the same setting with the opposite sense. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<keyboardcatchquit,keyboardCatchQuit>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singequit,singeQuit>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Ask before leaving a game in progress.
|
|
singeSetQuitKeyEnabled(false)
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_QUIT then
|
|
if confirmQuit() then
|
|
singeQuit()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singeversion]
|
|
==== singeVersion
|
|
|
|
[source,text]
|
|
----
|
|
version = singeVersion()
|
|
----
|
|
|
|
Returns the engine version as a number, major plus minor as a fraction, so `3.00` comes back as `3`. Compare it with `<` and `>=` to refuse an engine that is too old or to branch around features; `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR` and `SINGE_VERSION_STRING` are also set as globals for scripts that want the parts or the text.
|
|
|
|
*Returns:* number.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<singesetgamename,singeSetGameName>>
|
|
|
|
.Example
|
|
[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()
|
|
----
|
|
|
|
Returns `true` unless Singe was started with `--nocrosshair`. Draw the software targeting reticle only when it returns `true`; players with lightguns that put their own reticle on screen turn the software one off with that option. The value does not change during a run, so read it once.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<mousegetposition,mouseGetPosition>>, <<onmousemoved,onMouseMoved>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
drawCrosshair = singeWantsCrosshairs()
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
drawTargets()
|
|
if drawCrosshair then
|
|
spriteDraw(reticle, cursorX, cursorY, true)
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#soft]
|
|
=== Soft
|
|
|
|
Cloth, pressure bodies and ropes: meshes whose vertices are simulated as particles (see <<physics,Physics>>, Soft bodies). A soft body is addressed by its node, and a node without one terminates the script in every call after `softNew`. Positions are world units, mass kilograms, and the stiffness values run `0` to `1`. The engine rewrites the node's mesh from the particles after every step, and the simulation ends by itself when the node is deleted or given a different mesh. Up to 16 soft bodies at once, with up to 32 pins each.
|
|
|
|
[#softnew]
|
|
==== softNew
|
|
|
|
[source,text]
|
|
----
|
|
softNew(node, SOFT_CLOTH)
|
|
softNew(node, SOFT_BODY)
|
|
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
|
|
----
|
|
|
|
For `SOFT_CLOTH` and `SOFT_BODY` the node's mesh, of three vertices or more, becomes the particles: vertices at the same position (within `0.0001` units) are welded into one, placed where the node's transform puts them in the world, and the triangles give the stretch, shear and bend constraints. Cloth collides on both faces and is what an open sheet wants; a body is a closed mesh that `softSetPressure` can inflate. For `SOFT_ROPE` the node needs no mesh: `segments + 1` particles run in a line from the node's world position to the world point, the node is given a new six-sided tube mesh of that radius round them (keeping whatever material it had), and the particles collide as spheres of that radius, where cloth and body particles are spheres of radius `0.03`. Every kind starts with stretch `0.9`, bend `0.2`, mass `1` kilogram, damping `0.1` and pressure `0`. A soft body already on the node is removed before the new one is made, so a call that fails leaves the node with none; a rope's tube mesh goes with it, which is why a rope's node cannot become cloth or a body (the call fails for want of a mesh). Raises an error when physics is unavailable, the kind is unknown, the argument count does not fit the kind, the mesh is missing, a rope has no segments or no radius, or 16 soft bodies already exist.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the node carrying the mesh (cloth, body), or the rope's starting point.
|
|
* `x`, `y`, `z` -- the rope's far end, in world units.
|
|
* `segments` -- how many links the rope has, `1` or more; a dozen is plenty.
|
|
* `radius` -- the rope's thickness, in world units; also the size its particles collide with.
|
|
|
|
*Notes:* A mesh shared by several nodes is rewritten for all of them, so give a soft body a mesh of its own. Skinned meshes cannot be soft.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softpin,softPin>>, <<softsetstiffness,softSetStiffness>>, <<softsetpressure,softSetPressure>>, <<meshplane,meshPlane>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A flag hanging from the pole: an upright grid, pinned down its left edge.
|
|
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) -- Along the pole
|
|
end
|
|
----
|
|
|
|
[#softdelete]
|
|
==== softDelete
|
|
|
|
[source,text]
|
|
----
|
|
softDelete(node)
|
|
----
|
|
|
|
Ends the simulation and frees its body. The node keeps its mesh in the shape it was last written, so a dropped sheet stays draped over the crate and a rope keeps its tube; the original mesh shape is not restored. A node without a soft body terminates the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softnew,softNew>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Once the sheet has settled over the statue, freeze it and stop paying for it.
|
|
function onOverlayUpdate()
|
|
if covering then
|
|
settleFrames = settleFrames + 1
|
|
if settleFrames > 240 then
|
|
softDelete(sheet) -- The drape stays as it is
|
|
covering = false
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#softpin]
|
|
==== softPin
|
|
|
|
[source,text]
|
|
----
|
|
softPin(node, x, y, z)
|
|
softPin(node, x, y, z, otherNode)
|
|
----
|
|
|
|
Finds the particle nearest the world point, wherever the particles are right now, and fixes it. On its own the particle is held where it is; with `otherNode` it is moved to that node's world position before every step, so a rope hangs from a crane that swings and a cape follows the shoulders. The point need not lie on the mesh, since the nearest particle wins, and pinning the same particle again only changes what it follows. Past 32 pins the call does nothing.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the soft body's node.
|
|
* `x`, `y`, `z` -- a world point; the particle nearest it is pinned.
|
|
* `otherNode` -- a node for the particle to follow from now on.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softunpin,softUnpin>>, <<softnew,softNew>>, <<nodegetworldposition,nodeGetWorldPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A rope from the crane's hook out and down, swinging with the crane.
|
|
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) -- The top end rides the hook
|
|
----
|
|
|
|
[#softunpin]
|
|
==== softUnpin
|
|
|
|
[source,text]
|
|
----
|
|
softUnpin(node, x, y, z)
|
|
----
|
|
|
|
Frees the pinned particle nearest the world point. Nothing happens when that particle is not pinned. The body is remade around the current shape, which also stops every particle dead, so a flag let go of starts falling from rest.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softpin,softPin>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The banner's corner tears loose when the wind gust hits.
|
|
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)
|
|
----
|
|
|
|
How much the material resists being stretched (and, for cloth, sheared) and being folded, `0` to `1`; values outside are clamped. `1` is rigid, `0` the loosest the solver allows. Defaults `0.9` and `0.2`. The body is remade around the current shape: the rest lengths become the lengths of this moment, and every particle stops, so set the material up before the cloth starts to move. A rope has only stretch.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the soft body's node.
|
|
* `stretch` -- resistance to stretching, `0` to `1`; default `0.9`.
|
|
* `bend` -- resistance to folding, `0` to `1`; default `0.2`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softsetmass,softSetMass>>, <<softsetdamping,softSetDamping>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Silk, canvas and a rubber sheet from the same grid.
|
|
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)
|
|
----
|
|
|
|
Inflates a `SOFT_BODY`: `0` (default) is a deflated bag and a few units a firm balloon; a negative value becomes `0`. It changes the live body without remaking it and wakes it, so a balloon can be pumped up or let down while it bounces. Cloth and rope have no volume to inflate.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softnew,softNew>>, <<softsetstiffness,softSetStiffness>>, <<meshsphere,meshSphere>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
softNew(balloon, SOFT_BODY) -- A meshSphere of 0.5
|
|
softSetMass(balloon, 0.6)
|
|
softSetPressure(balloon, 8) -- A firm balloon
|
|
|
|
function onOverlayUpdate()
|
|
if punctured then
|
|
pressure = math.max(pressure - 0.1, 0)
|
|
softSetPressure(balloon, pressure) -- Hissing down over a couple of seconds
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#softsetmass]
|
|
==== softSetMass
|
|
|
|
[source,text]
|
|
----
|
|
softSetMass(node, kilograms)
|
|
----
|
|
|
|
The whole body's mass, shared evenly between its particles; default `1`, kept at `0.001` or more. Pinned particles weigh nothing regardless. The body is remade around the current shape and every particle stops, so set it up front.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softsetstiffness,softSetStiffness>>, <<softsetdamping,softSetDamping>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A sheet pinned by four corners catches a crate dropped onto it.
|
|
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) -- Heavy enough not to flutter
|
|
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)
|
|
----
|
|
|
|
How quickly the particles' motion dies away, `0` upward; default `0.1`, and a negative value becomes `0`. Higher values calm a flag that will not stop rippling or a rope that swings forever. The body is remade around the current shape and every particle stops.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<softsetmass,softSetMass>>, <<softsetstiffness,softSetStiffness>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A curtain in still air settles fast; the same curtain by the open window keeps moving.
|
|
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
|
|
|
|
Sound effects are audio clips (WAV is the safe choice) loaded into memory and mixed through SDL_mixer, separate from the disc's own audio and from the audio of `video*` assets. `soundLoad` returns a sound handle that identifies the clip's data; `soundPlay` starts that clip on one of sixteen mixer channels and returns the channel number, `0` through `15`, which is what `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` and every `soundSet*` call take. One clip can play on several channels at once, and a channel number is reused as soon as the channel is free, so keep the two kinds of number apart. A master effects volume (`soundSetVolume`, `0` to `63`) scales every channel. A playing channel can also sit in the 3D scene, following a node or fixed at a point in world units, where it is panned and faded by its distance from the listener (see <<scenes3d,3D Scenes>>); for 2D games `soundSetPan` pans a channel without a scene.
|
|
|
|
[#soundfullstop]
|
|
==== soundFullStop
|
|
|
|
[source,text]
|
|
----
|
|
soundFullStop()
|
|
----
|
|
|
|
Stops every effect channel at once, paused ones included, and frees them for reuse. No clip is unloaded; the handles from `soundLoad` stay valid. Every channel that was playing or paused reports through `onSoundCompleted`, exactly as it does after `soundStop`. Call it on a scene change or before a cutscene rather than tracking every channel you started.
|
|
|
|
*Since:* 1.16
|
|
*See also:* <<soundstop,soundStop>>, <<onsoundcompleted,onSoundCompleted>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function enterAttractMode()
|
|
soundFullStop()
|
|
ambientChannel = -1
|
|
engineChannel = -1
|
|
discPlay()
|
|
end
|
|
----
|
|
|
|
[#soundgetposition]
|
|
==== soundGetPosition
|
|
|
|
[source,text]
|
|
----
|
|
x, y, z, gain = soundGetPosition(channel)
|
|
----
|
|
|
|
Reports where a positioned channel sits relative to the listener, as a direction of unit length in the listener's frame (`x` positive to the right, `y` positive up, `z` positive behind the listener), and the volume factor its distance currently gives it, `0` to `1`. The values are refreshed once per frame from the listener and the channel's node or point. A channel that has not been placed in the scene since it started returns `0, 0, 0, 1`. Meant for radar-style indicators and for checking a mix from a test script; a channel outside `0` to `15` aborts the script.
|
|
|
|
*Returns:* three numbers for the direction and one for the distance gain.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Point an arrow on the HUD toward the nearest audible enemy.
|
|
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()
|
|
----
|
|
|
|
Returns the master effects volume, `0` through `63`. At startup it reflects the effects volume from the command line, so a game that shows a volume setting should read it rather than assume the maximum.
|
|
|
|
*Returns:* integer, `0` to `63`.
|
|
|
|
*Since:* 1.16
|
|
*See also:* <<soundsetvolume,soundSetVolume>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Duck the effects while the narrator speaks, then put them back.
|
|
function narratorBegin()
|
|
savedVolume = soundGetVolume()
|
|
soundSetVolume(math.floor(savedVolume / 3))
|
|
narratorChannel = soundPlay(narratorClip)
|
|
end
|
|
----
|
|
|
|
[#soundisplaying]
|
|
==== soundIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = soundIsPlaying(channel)
|
|
----
|
|
|
|
Tests whether a channel is playing right now. A paused channel reports `false`, as does a channel whose clip has finished. A channel outside `0` to `15` aborts the script; a channel that has merely gone quiet does not.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 1.16 (RDG)
|
|
*See also:* <<soundplay,soundPlay>>, <<soundpause,soundPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Do not restart the alarm while it is still sounding.
|
|
function raiseAlarm()
|
|
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmClip)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#soundload]
|
|
==== soundLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = soundLoad(filename)
|
|
----
|
|
|
|
Loads an audio clip into memory and returns its handle. The path goes through the game's virtual file system, so it works the same from a loose directory and from a packed game. Load clips once at startup, not inside a callback that fires every frame. A file that cannot be opened or decoded aborts the script.
|
|
|
|
*Returns:* integer sound handle. This is a data handle, not a channel number.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<soundplay,soundPlay>>, <<soundunload,soundUnload>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Clip handles, loaded once and unloaded in onShutdown.
|
|
shotClip = soundLoad(DIR .. "sounds/shot.wav")
|
|
missClip = soundLoad(DIR .. "sounds/miss.wav")
|
|
coinClip = soundLoad(DIR .. "sounds/coin.wav")
|
|
ambientClip = soundLoad(DIR .. "sounds/wind.wav")
|
|
----
|
|
|
|
[#soundloaddata]
|
|
==== soundLoadData
|
|
|
|
[source,text]
|
|
----
|
|
id = soundLoadData(data)
|
|
----
|
|
|
|
Hypseus extension. Builds a sound from bytes held in a Lua string instead of from a file. Every format <<soundload,`soundLoad`>> reads is read here, and the handle it returns is used with the rest of the `sound` family in exactly the same way.
|
|
|
|
The string may hold zero bytes; its length, not its first zero, decides how much is read. The bytes are decoded during the call and the string is not kept, so it may be discarded afterwards. Bytes that are not audio, or an empty string, terminate the script, as an unreadable file does in `soundLoad`.
|
|
|
|
Use it for audio a game generates, unpacks or decrypts for itself. Audio the virtual file system can reach by name -- including audio inside a packed `.game` -- is better loaded with `soundLoad`, which does not hold the whole file in a Lua string.
|
|
|
|
*Parameters:*
|
|
|
|
* `data` -- string of bytes.
|
|
|
|
*Returns:* number, the sound handle.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<soundload,soundLoad>>, <<spriteloaddata,spriteLoadData>>, <<soundplay,soundPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A sound effect the game unpacks from its own archive.
|
|
local bytes = myArchive:extract("beep.wav")
|
|
local beep = soundLoadData(bytes)
|
|
soundPlay(beep)
|
|
----
|
|
|
|
[#soundpause]
|
|
==== soundPause
|
|
|
|
[source,text]
|
|
----
|
|
wasPlaying = soundPause(channel)
|
|
----
|
|
|
|
Pauses a channel in place; `soundResume` continues it from the same point. A paused channel still counts as busy, so `soundPlay` will not hand it out to another clip, and `soundIsPlaying` reports `false` for it. Pausing a channel that is not playing does nothing. A channel outside `0` to `15` aborts the script.
|
|
|
|
*Returns:* boolean, `true` when the channel was playing at the moment of the call.
|
|
|
|
*Since:* 1.16 (RDG)
|
|
*See also:* <<soundresume,soundResume>>, <<soundisplaying,soundIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hold the engine noise while the radio message plays.
|
|
function radioBegin(messageClip)
|
|
engineWasRunning = soundPause(engineChannel)
|
|
radioChannel = soundPlay(messageClip)
|
|
end
|
|
----
|
|
|
|
[#soundplay]
|
|
==== soundPlay
|
|
|
|
[source,text]
|
|
----
|
|
channel = soundPlay(id)
|
|
channel = soundPlay(id, loops)
|
|
----
|
|
|
|
Starts a loaded clip on the first free channel and returns that channel's number. A channel counts as free when it is neither playing nor paused. The new channel starts at the master volume, centered, with no scene position and the default range, whatever the channel's previous use set. When all sixteen channels are busy nothing plays and `-1` (`SOUND_ERROR_INVALID`) comes back; a game that fires many overlapping sounds should check for it. A handle that is not loaded aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sound handle from `soundLoad`.
|
|
* `loops` -- `0` (the default) plays the clip once, a positive count repeats it that many more times, `-1` loops it until it is stopped. Anything below `-1` aborts the script.
|
|
|
|
*Returns:* integer channel number, `0` to `15`, or `-1` when no channel is free.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetnode,soundSetNode>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Continues a channel that `soundPause` stopped, from where it left off. Resuming a channel that is not paused does nothing. A channel outside `0` to `15` aborts the script.
|
|
|
|
*Returns:* boolean, `true` when the channel was paused at the moment of the call.
|
|
|
|
*Since:* 1.16 (RDG)
|
|
*See also:* <<soundpause,soundPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
if channel == radioChannel then
|
|
radioChannel = -1
|
|
if engineWasRunning then
|
|
soundResume(engineChannel)
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
[#soundsetlistener]
|
|
==== soundSetListener
|
|
|
|
[source,text]
|
|
----
|
|
soundSetListener(node)
|
|
soundSetListener()
|
|
----
|
|
|
|
Chooses the node that positioned channels are heard from, facing the way it faces. With no argument, or `nil`, the listener is the scene camera, which is right for a first-person or fixed-camera game; a third-person game usually prefers the player's node so that sounds pan around the character rather than the camera. A node that is later deleted drops the listener back to the camera. A node handle that does not exist aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<cameraset,cameraSet>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Chase camera, but hear the world from the car.
|
|
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)
|
|
----
|
|
|
|
Attaches a playing channel to a scene node so it is heard from wherever the node is each frame: the channel is panned toward the node's direction from the listener and faded by its distance according to `soundSetRange`. Calling it with no node, or `nil`, takes the channel back out of the scene at full master volume. A channel whose node is deleted drops back to plain playback on its own. The attachment lasts until the channel is stopped or reused by `soundPlay`. A channel outside `0` to `15`, or a node that does not exist, aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>, <<soundsetlistener,soundSetListener>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Each brazier crackles from where it stands.
|
|
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)
|
|
----
|
|
|
|
Pans a channel across the stereo pair for games without a scene: `-1` is fully left, `1` fully right, `0` centered, and values outside that range are clamped. Moving away from center reduces the far speaker only; at `pan = 0.5` the left speaker plays at half gain and the right at full. The call takes the channel out of the scene if `soundSetNode` or `soundSetPosition` had placed it there. A channel outside `0` to `15` aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<soundplay,soundPlay>>, <<soundsetnode,soundSetNode>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Pan a passing car by where it is on the overlay.
|
|
function carPassBy(x)
|
|
local channel = soundPlay(carClip)
|
|
if channel >= 0 then
|
|
soundSetPan(channel, (x / overlayGetWidth()) * 2 - 1)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#soundsetposition]
|
|
==== soundSetPosition
|
|
|
|
[source,text]
|
|
----
|
|
soundSetPosition(channel, x, y, z)
|
|
----
|
|
|
|
Places a playing channel at a fixed point in the scene, in world units, replacing any node it was following. It is then panned and faded from the listener like a node-attached channel. Suits a sound that stays put, such as a waterfall or a ringing telephone; a moving source is easier with `soundSetNode`. A channel outside `0` to `15` aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<soundsetnode,soundSetNode>>, <<soundsetrange,soundSetRange>>, <<soundgetposition,soundGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The waterfall never moves, so no node is needed.
|
|
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)
|
|
----
|
|
|
|
Sets how a positioned channel fades with distance from the listener. Within `near` it plays at full volume; beyond that its gain is `near` divided by the distance; over the last fifth of `far` it also fades linearly, reaching silence at `far`. Defaults are `1` and `30` world units, restored whenever `soundPlay` reuses the channel. A whisper wants a small `far`, a waterfall a large one.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- channel number from `soundPlay`.
|
|
* `near` -- distance within which the channel is at full volume, greater than `0`.
|
|
* `far` -- distance at which it is silent, greater than `near`. Any other pair aborts the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundgetposition,soundGetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A guard's footsteps are only audible nearby; the siren carries.
|
|
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)
|
|
----
|
|
|
|
Sets the master effects volume and applies it at once to every channel, playing or paused. Positioned channels keep their distance fade on top of it. Values outside `0` to `63` abort the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `volume` -- integer, `0` (silent) through `63` (loudest).
|
|
|
|
*Since:* 1.16
|
|
*See also:* <<soundgetvolume,soundGetVolume>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Service menu volume slider driven by the left and right switches.
|
|
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)
|
|
----
|
|
|
|
Halts a channel immediately, whether playing or paused, and frees it for reuse. The channel reports through `onSoundCompleted` just as it does when the clip finishes on its own, so a script that chains sounds from that callback should check whether it stopped the channel itself. The next `soundPlay` may hand the same number out again, so discard the number on the Lua side. A channel outside `0` to `15` aborts the script.
|
|
|
|
*Returns:* boolean, `true` when the channel was playing at the moment of the call. A paused channel returns `false` but is still stopped.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<soundfullstop,soundFullStop>>, <<onsoundcompleted,onSoundCompleted>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Frees a loaded clip and invalidates its handle. The mixer keeps the clip's data alive until every channel still playing it has stopped, so unloading does not cut a playing channel short. Any later use of the handle, including another `soundUnload`, aborts the script. The usual place for it is `onShutdown`.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sound handle from `soundLoad`, not a channel number.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<soundload,soundLoad>>, <<onshutdown,onShutdown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
soundUnload(shotClip)
|
|
soundUnload(missClip)
|
|
soundUnload(coinClip)
|
|
soundUnload(ambientClip)
|
|
fontUnload(hudFont)
|
|
end
|
|
----
|
|
|
|
[#sprite]
|
|
=== Sprite
|
|
|
|
Sprites are 2D bitmaps loaded from disk with `spriteLoad`, or rendered from text with `fontToSprite`, and drawn onto the overlay in overlay coordinates. Every `sprite*` call takes the integer handle as its first argument (Singe 2.10 put it last; see <<migrating,Migrating from Singe 2.10>>), and a handle that has been unloaded ends the script with an error. Any format SDL_image reads is accepted; an animated GIF or WEBP with two or more frames loads as an animation that sits on frame `0`, stopped and non-looping, until `spritePlay` and `spriteLoop` say otherwise. Pixels whose raw value is `0` in the image's own pixel format are drawn transparent (palette index `0`, or black in an RGB image without alpha), and a real alpha channel is honored as well. Release every sprite with `spriteUnload`, usually from <<onshutdown,onShutdown>>.
|
|
|
|
*Sprite sheets.* Hypseus Singe extends the family with sprite sheets: `spriteLoadFrames` slices a single row strip of equal frames into an animation that is not playing, and `spriteDrawFrame` draws one chosen frame (1-based) without touching playback. A sheet is otherwise an ordinary animation, so `spriteSetFrame` (0-based), `spritePlay`, `spriteDraw`, `nodeSetSprite` and `emitterSetTexture` all accept it, and `spriteFrameWidth` and `spriteFrameHeight` report the unscaled size of one frame. The entries marked "Hypseus extension" exist so games written for Hypseus Singe run unchanged.
|
|
|
|
[#spritedraw]
|
|
==== spriteBlend
|
|
|
|
[source,text]
|
|
----
|
|
spriteBlend(boolean, sprite)
|
|
----
|
|
|
|
Hypseus extension, accepted and ignored. It is a workaround for that engine: Hypseus loads sprites with SDL's blend mode off, and this turns it back on for one sprite. Singe draws every sprite with its alpha channel already, so blending is never off to begin with. The sprite handle is still checked, so a bad one is still an error.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00
|
|
|
|
==== 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)
|
|
----
|
|
|
|
Blits the sprite onto the overlay as currently rotated and scaled. Call it from `onOverlayUpdate`. The first two forms draw at the size `spriteGetWidth` and `spriteGetHeight` report, with `(x, y)` as the top-left corner; the last two stretch that image into the rectangle from `(x, y)` to `(x2, y2)` inclusive, always with nearest-neighbor sampling whatever `spriteQuality` says. If the sprite is a playing animation, the draw first advances it by the real time elapsed since the previous draw (or since `spritePlay`), stepping through as many frames as that time covers, and shows each frame for at least 10 ms even when the file asks for less. When the last frame passes, playback wraps to frame `0` if `spriteLoop` is set, otherwise it stays on the last frame and stops. Parts that fall outside the overlay are clipped.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle from `spriteLoad` or `fontToSprite`.
|
|
* `x`, `y` -- anchor in overlay coordinates; fractions are truncated.
|
|
* `x2`, `y2` -- opposite corner of the target rectangle, inclusive, for the stretched forms.
|
|
* `centered` -- boolean. When `true`, the image (or the stretched rectangle) is shifted so that `(x, y)` is its center instead of its top-left corner.
|
|
|
|
*Notes:* A playing animation that is not drawn for a while catches up on its next draw; call `spritePause` if it should hold its frame instead. Use the centered form for rotated sprites, since rotation changes the bounding box and a top-left anchor appears to drift.
|
|
|
|
*Since:* 1.x. Centered and stretched-centered forms added in 2.10.
|
|
*See also:* <<spriteload,spriteLoad>>, <<spriterotate,spriteRotate>>, <<spritegetwidth,spriteGetWidth>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Cabinet art at natural size, the attract still squeezed into its box,
|
|
-- and a cursor anchored on its center.
|
|
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 extension. Blits one frame of a sprite sheet (or of any animation) onto the overlay with `(x, y)` as its top-left corner, without changing which frame the sprite is parked on or whether it is playing. Frames are numbered from `1`, as Hypseus counts them; a frame outside `1` to the frame count draws frame `1`. A still image draws itself and ignores `frame`. The sprite's own rotation and scale from `spriteRotate` and `spriteScale` are ignored: the first form draws the frame at its natural size, the others multiply the frame's width by `scaleX` (or `scale`) and its height by `scaleY` (or `scale`) with nearest-neighbor sampling. Call it from `onOverlayUpdate`.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle from `spriteLoadFrames` or `spriteLoad`.
|
|
* `x`, `y` -- top-left corner in overlay coordinates; fractions are truncated.
|
|
* `frame` -- frame number, `1` to the frame count; fractions are truncated.
|
|
* `scale`, `scaleX`, `scaleY` -- size multipliers, `1` for natural size.
|
|
|
|
*Notes:* `spriteSetFrame` counts frames from `0`; `spriteDrawFrame` counts from `1`.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriteloadframes,spriteLoadFrames>>, <<spritedraw,spriteDraw>>, <<spritesetframe,spriteSetFrame>>, <<spriteframewidth,spriteFrameWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A ten frame digit strip draws the score, one digit per 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)
|
|
----
|
|
|
|
Hypseus extension. Draws one rectangle out of a sprite, at its own size, with its top left corner at `x`, `y`. It is the only call that takes a source rectangle: <<spritedraw,`spriteDraw`>> stretches the whole sprite into a destination and <<spritedrawframe,`spriteDrawFrame`>> takes a whole frame of a sheet, while this takes any rectangle at all -- which is how a game cuts a tile, a digit or a letter out of one image without loading it many times over.
|
|
|
|
The rectangle is in the sprite's own pixels and must lie inside it; a rectangle that does not, or one with no width or height, terminates the script naming the sprite's size. The sprite drawn from is the sprite as it is currently drawn, so a <<spriterotate,`spriteRotate`>> or a <<spritescale,`spriteScale`>> changes what the coordinates mean.
|
|
|
|
Under `LEGACY_SPRITE_ARGS` the handle is third, `spriteDrawGrid(x, y, spr, sourceX, sourceY, width, height)`, which is where Hypseus puts it.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; a sprite handle.
|
|
* `x`, `y` -- numbers; where the rectangle's top left corner lands, in overlay coordinates.
|
|
* `sourceX`, `sourceY` -- numbers; the rectangle's top left corner in the sprite.
|
|
* `width`, `height` -- numbers; its size, at least 1.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spritedraw,spriteDraw>>, <<spritedrawframe,spriteDrawFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A number drawn from one strip of ten digits, each 16 by 24.
|
|
local function drawNumber(sheet, x, y, value)
|
|
for i = 1, #value do
|
|
local digit = tonumber(value:sub(i, i))
|
|
spriteDrawGrid(sheet, x + (i - 1) * 16, y, digit * 16, 0, 16, 24)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#spritedrawrotatedframe]
|
|
==== spriteDrawRotatedFrame
|
|
|
|
[source,text]
|
|
----
|
|
spriteDrawRotatedFrame(id, x, y)
|
|
spriteDrawRotatedFrame(id, x, y, scale)
|
|
spriteDrawRotatedFrame(id, x, y, scaleX, scaleY)
|
|
----
|
|
|
|
Hypseus extension. Draws the frame <<spriterotateframe,`spriteRotateFrame`>> last turned, *centred* on `x`, `y`. This is the pair to that call and the reason both exist: <<spritedrawframe,`spriteDrawFrame`>> deliberately ignores a sprite's rotation and scale, so a game that wants one frame of a sheet turned uses these two instead.
|
|
|
|
Given a scale, the turned frame is stretched by it as it is drawn -- on top of whatever scale it was turned with -- and the result is still centred on `x`, `y`. `scaleX` alone scales both axes.
|
|
|
|
Drawing before any `spriteRotateFrame` terminates the script, since there is no turned frame to draw.
|
|
|
|
Under `LEGACY_SPRITE_ARGS` the handle is last, as Hypseus writes it.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; a sprite handle.
|
|
* `x`, `y` -- numbers; the *centre* of the drawn frame, in overlay coordinates.
|
|
* `scale` or `scaleX`, `scaleY` -- optional numbers; a further scale at draw time.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriterotateframe,spriteRotateFrame>>, <<spritedrawframe,spriteDrawFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A compass needle: one frame of the sheet, pointing where the player is going.
|
|
spriteRotateFrame(needle, heading, 1)
|
|
spriteDrawRotatedFrame(needle, 320, 240)
|
|
----
|
|
|
|
[#spriteframeheight]
|
|
==== spriteFrameHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = spriteFrameHeight(id)
|
|
----
|
|
|
|
Hypseus extension. Returns the height of one frame of the sprite before any scaling or rotation: the strip's height for a `spriteLoadFrames` sheet, a frame's height for an animation, and the image's height for a still (Hypseus reports the drawn surface here). `spriteGetHeight` reports the size as drawn instead.
|
|
|
|
*Returns:* integer.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriteframewidth,spriteFrameWidth>>, <<spritegetheight,spriteGetHeight>>, <<spriteloadframes,spriteLoadFrames>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Centre a frame on a point.
|
|
function drawCentred(sheet, frame, x, y)
|
|
spriteDrawFrame(sheet, x - spriteFrameWidth(sheet) / 2, y - spriteFrameHeight(sheet) / 2, frame)
|
|
end
|
|
----
|
|
|
|
[#spriteframewidth]
|
|
==== spriteFrameWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = spriteFrameWidth(id)
|
|
----
|
|
|
|
Hypseus extension. Returns the width of one frame of the sprite before any scaling or rotation: the strip's width divided by the frame count for a `spriteLoadFrames` sheet, a frame's width for an animation, and the image's width for a still (Hypseus reports `0` there). `spriteGetWidth` reports the size as drawn instead.
|
|
|
|
*Returns:* integer.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriteframeheight,spriteFrameHeight>>, <<spritegetwidth,spriteGetWidth>>, <<spriteloadframes,spriteLoadFrames>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Lay the frames of a strip out side by side.
|
|
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)
|
|
----
|
|
|
|
Returns the zero-based index of the frame the sprite currently shows. Still images and animations that have not advanced report `0`. Frames advance only inside `spriteDraw`, so the value changes between draws, not between arbitrary calls. Use it to line up sound or game logic with a particular frame of an animation.
|
|
|
|
*Returns:* integer frame index, `0` for a still image.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritesetframe,spriteSetFrame>>, <<spriteisplaying,spriteIsPlaying>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fire the gunshot on the frame where the muzzle flash appears.
|
|
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 extension. How many frames a sprite has: the frame count of a sheet loaded by <<spriteloadframes,`spriteLoadFrames`>> or of an animated GIF or WEBP, and `0` for a still image. It is the count <<spritesetframe,`spriteSetFrame`>>'s zero based frame number runs up to, and one more than the largest it will take.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; a sprite handle.
|
|
|
|
*Returns:* number; `0` for a still image.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriteloadframes,spriteLoadFrames>>, <<spritegetframe,spriteGetFrame>>, <<spritesetframe,spriteSetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Step to the next frame, whatever the sheet turned out to be.
|
|
local frames = spriteGetFrames(walk)
|
|
if frames > 0 then
|
|
spriteSetFrame(walk, (spriteGetFrame(walk) + 1) % frames)
|
|
end
|
|
----
|
|
|
|
[#spritegetheight]
|
|
==== spriteGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
result = spriteGetHeight(id)
|
|
----
|
|
|
|
Returns the height of the sprite as it would be drawn right now, in overlay coordinates. Scaling and rotation applied with `spriteScale`, `spriteRotate`, or `spriteRotateAndScale` are included, so a rotated sprite reports the height of its rotated bounding box, not of the source image. For an animation the current frame is measured. The stretched forms of `spriteDraw` do not affect the value.
|
|
|
|
*Returns:* integer height.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<spritegetwidth,spriteGetWidth>>, <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Stack pre-rendered description lines, spaced by the height of the 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)
|
|
----
|
|
|
|
Returns the width of the sprite as it would be drawn right now, in overlay coordinates, including any scaling and rotation currently applied. For an animation the current frame is measured. Together with `spriteGetHeight` this is the usual way to center a sprite in a box or to measure text rendered by `fontToSprite`.
|
|
|
|
*Returns:* integer width.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<spritegetheight,spriteGetHeight>>, <<spritescale,spriteScale>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Center the cabinet image inside its panel, and the title across the overlay.
|
|
function onOverlayUpdate()
|
|
local x = CABINET_X + (CABINET_W - spriteGetWidth(cabinetSprite)) * 0.5
|
|
local y = CABINET_Y + (CABINET_H - spriteGetHeight(cabinetSprite)) * 0.5
|
|
spriteDraw(cabinetSprite, x, y)
|
|
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#spriteisplaying]
|
|
==== spriteIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
result = spriteIsPlaying(id)
|
|
----
|
|
|
|
Reports whether the sprite's animation is currently advancing. A freshly loaded sprite reports `false`; `spritePlay` turns it on, and `spritePause` or the end of a non-looping animation turns it off. Poll it to find out when a one-shot animation has finished.
|
|
|
|
*Returns:* boolean, `true` while the animation is playing.
|
|
|
|
*Notes:* `spritePlay` and `spritePause` do not check for animation data, so a still image reports whatever those calls last set.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteloop,spriteLoop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hold the menu until the one-shot logo animation has finished.
|
|
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)
|
|
----
|
|
|
|
Loads an image and returns an integer handle for the other `sprite*` calls. The name goes through the engine's virtual file system, so it may live loose on disk or inside a packed game; it is resolved relative to the directory Singe was started in, so prepend `DIR` for files shipped with your game. Every format in <<formats,Video, Audio, and Container Formats>> is read. An animated GIF or WEBP with two or more frames loads as an animation, parked on frame `0`, not playing and not looping; every other file, including a single-frame GIF, loads as a still image. An image with more than 8 bits per channel, or floating point pixels, is converted to 8-bit RGBA on load and treated as one from then on. Pixels with a raw value of `0` in the file's own pixel format become transparent. Loading is synchronous, and a file that cannot be opened or decoded ends the script with the loader's error message rather than returning `nil`.
|
|
|
|
The optional size is for a vector picture -- an SVG -- which has no size of its own worth having: the picture is rasterised to fit that box, keeping its own proportions, rather than rasterised at the file's size and scaled up. Ask <<spritegetwidth,`spriteGetWidth`>> and <<spritegetheight,`spriteGetHeight`>> what it came out as, because fitting a square drawing into a wide box leaves the width unused. Every other format ignores the size.
|
|
|
|
*Parameters:*
|
|
|
|
* `filename` -- string; the image to load.
|
|
* `width` -- optional number; the width to fit an SVG into, `1` or more.
|
|
* `height` -- optional number; the height to fit an SVG into, `1` or more. Both must be given together.
|
|
|
|
*Returns:* integer sprite handle.
|
|
|
|
*Notes:* Animations do not start by themselves. Call `spriteLoop(id, true)` and `spritePlay(id)` after loading if the animation should run and repeat.
|
|
|
|
*Since:* 1.x. Animated GIF/WEBP support added in 2.10; the vector size in 3.00.
|
|
*See also:* <<spriteunload,spriteUnload>>, <<spriteplay,spritePlay>>, <<spriteloop,spriteLoop>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Load the menu art once at startup; the attract loop is an animated 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 extension. Builds a sprite from an image held in a Lua string instead of from a file. Every format <<spriteload,`spriteLoad`>> reads is read here, animated GIF and WEBP included, and an animation loaded this way behaves exactly as one loaded from a file.
|
|
|
|
The string may hold zero bytes; its length, not its first zero, decides how much is read. The image is decoded during the call and the string is not kept. Bytes that are not an image, or an empty string, terminate the script, as an unreadable file does in `spriteLoad`.
|
|
|
|
Use it for artwork a game generates, unpacks or decrypts for itself. Artwork the virtual file system can reach by name -- including artwork inside a packed `.game` -- is better loaded with `spriteLoad`, which does not hold the whole file in a Lua string.
|
|
|
|
*Parameters:*
|
|
|
|
* `data` -- string of bytes.
|
|
|
|
*Returns:* number, the sprite handle.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriteload,spriteLoad>>, <<soundloaddata,soundLoadData>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- An image the game keeps inside its own data file.
|
|
local bytes = myArchive:extract("logo.png")
|
|
local logo = spriteLoadData(bytes)
|
|
spriteDraw(logo, 10, 10)
|
|
----
|
|
|
|
[#spriteloadframes]
|
|
==== spriteLoadFrames
|
|
|
|
[source,text]
|
|
----
|
|
result = spriteLoadFrames(count, filename)
|
|
----
|
|
|
|
Hypseus extension. Loads an image holding `count` equal frames in a single row and returns a sprite handle. The frame width is the image width divided by `count` (integer division; leftover columns on the right are dropped) and the frame height is the image height. The sheet becomes an ordinary animation parked on frame `0`, not playing and not looping, so `spriteDraw` shows the current frame rather than the whole strip (as Hypseus draws it), and `spriteDrawFrame` picks any frame directly. The name goes through the virtual file system like `spriteLoad`, and pixels with a raw value of `0` become transparent in every frame. A count outside `2` to `512`, an image narrower than `count` pixels, or a file that cannot be read ends the script with an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `count` -- number of frames across the image, `2` to `512`.
|
|
* `filename` -- the strip image.
|
|
|
|
*Returns:* integer sprite handle.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spritedrawframe,spriteDrawFrame>>, <<spriteframewidth,spriteFrameWidth>>, <<spriteload,spriteLoad>>, <<spriteunload,spriteUnload>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A five frame car strip; frame 3 is the car pointing straight ahead.
|
|
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)
|
|
----
|
|
|
|
Sets whether the animation restarts from frame `0` after its last frame or stops there. New sprites do not loop. The setting is consulted each time `spriteDraw` steps past the last frame, so it can be changed while the animation runs. Setting it on a sprite that has already stopped does not restart playback; call `spritePlay` for that. The argument must be a Lua boolean.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle.
|
|
* `loop` -- boolean. `true` wraps to frame `0`; `false` (the default) stops on the last frame and `spriteIsPlaying` starts returning `false`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>, <<spritesetframe,spriteSetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The torch flickers forever; the explosion plays once per hit.
|
|
torchFlame = spriteLoad(DIR .. "torch.gif")
|
|
explosion = spriteLoad(DIR .. "explosion.gif")
|
|
spriteLoop(torchFlame, true)
|
|
spriteLoop(explosion, false)
|
|
spritePlay(torchFlame)
|
|
----
|
|
|
|
[#spritepause]
|
|
==== spritePause
|
|
|
|
[source,text]
|
|
----
|
|
spritePause(id)
|
|
----
|
|
|
|
Stops the animation on its current frame. The frame keeps being drawn by `spriteDraw`, and the time spent paused is not counted when `spritePlay` resumes, so the frame does not skip ahead. Pausing a sprite that is not playing changes nothing.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Freeze the enemy mid-stride while the hit flash plays.
|
|
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)
|
|
----
|
|
|
|
Starts or resumes the animation from its current frame, timing the first step from the moment of the call. A sprite that is already playing is left alone, so the frame timer is not reset. A new sprite must be started with this call; loading alone does not animate it.
|
|
|
|
*Notes:* A non-looping animation that has reached its last frame stays on that frame when played again. To replay it, call `spriteSetFrame(id, 0)` first.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritepause,spritePause>>, <<spritesetframe,spriteSetFrame>>, <<spriteloop,spriteLoop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Restart the one-shot explosion at the point of impact.
|
|
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)
|
|
----
|
|
|
|
Chooses the sampling used when the sprite is rebuilt for rotation or scaling. `RENDER_PIXELATED` keeps hard pixel edges, `RENDER_SMOOTH` interpolates. New sprites are pixelated. The setting only matters while a rotation or a scale other than `1.0` is in effect; an untransformed sprite is drawn straight from its source image, and the stretched forms of `spriteDraw` always use nearest-neighbor sampling. Changing the mode rebuilds the transformed image at once, so set it once after loading rather than every frame.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle.
|
|
* `mode` -- `RENDER_PIXELATED` (`0`) or `RENDER_SMOOTH` (`1`). Any nonzero number selects smooth.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>, <<spriterotateandscale,spriteRotateAndScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Photographic marquee art should interpolate; pixel art must stay crisp.
|
|
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 extension. Accepted for compatibility and does nothing. In Hypseus the flag re-applies the transparent colour to the copies `spriteRotate` and `spriteScale` produce, which would otherwise draw their background opaque; Singe's transformed copies carry the transparency in their alpha channel, so there is nothing to re-key. A bad handle or a non-boolean argument aborts the script, as for every other sprite call.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle.
|
|
* `enabled` -- boolean, ignored.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spriteload,spriteLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hypseus game code; harmless in Singe.
|
|
badge = spriteLoad(DIR .. "badge.png")
|
|
spriteResetColorKey(badge, true)
|
|
----
|
|
|
|
[#spriterotate]
|
|
==== spriteRotate
|
|
|
|
[source,text]
|
|
----
|
|
spriteRotate(id, degrees)
|
|
----
|
|
|
|
Rotates the sprite clockwise about its center. The angle is reduced modulo 360 and may be fractional or negative. The current scale is kept. The rotated image is rebuilt only when the angle actually changes, so repeating the same angle every frame costs nothing, while a new angle every frame rebuilds every frame. Rotation enlarges the bounding box, so `spriteGetWidth` and `spriteGetHeight` change and the centered form of `spriteDraw` is the one to use.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritedraw,spriteDraw>>, <<spritequality,spriteQuality>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Crates from a 2D physics scene, drawn at the angle of their bodies.
|
|
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)
|
|
----
|
|
|
|
Sets rotation and scale together. The result is the same as `spriteRotate` followed by `spriteScale`, but the transformed image is rebuilt once instead of twice, and not at all when nothing changed. Scale factors must be greater than `0`; a zero or negative factor cannot be rendered and ends the game with an error.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle.
|
|
* `degrees` -- clockwise rotation, reduced modulo 360.
|
|
* `scale` -- uniform factor, `1.0` for natural size.
|
|
* `scaleX`, `scaleY` -- separate horizontal and vertical factors.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spritequality,spriteQuality>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A coin that spins and shrinks as it flies toward the score counter.
|
|
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 extension. Takes one frame out of a sprite sheet, turns it clockwise by `degrees` and scales it by the sprite's own <<spritescale,`spriteScale`>>, and keeps the result for <<spritedrawrotatedframe,`spriteDrawRotatedFrame`>>. The sprite's own drawn surface is untouched, so <<spritedraw,`spriteDraw`>> and `spriteDrawFrame` go on showing what they showed.
|
|
|
|
Frames are numbered from 1, as `spriteDrawFrame` numbers them (and unlike `spriteGetFrame` and `spriteSetFrame`, which count from 0); a frame outside the sheet is frame 1 rather than an error. A still image turns its whole picture.
|
|
|
|
Turning is not free: the frame is redrawn pixel by pixel each time, so turn it when the angle changes rather than every frame. The angle is taken modulo 360, and <<spritequality,`spriteQuality`>> decides whether the turned edges are smoothed.
|
|
|
|
Under `LEGACY_SPRITE_ARGS` the handle is last, `spriteRotateFrame(degrees, frame, spr)`, as Hypseus writes it.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; a sprite handle.
|
|
* `degrees` -- number; clockwise.
|
|
* `frame` -- number; 1 to the frame count.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<spritedrawrotatedframe,spriteDrawRotatedFrame>>, <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A ship sprite whose hull is frame 1 and whose thrust is frame 2.
|
|
spriteRotateFrame(ship, angle, thrusting and 2 or 1)
|
|
spriteDrawRotatedFrame(ship, shipX, shipY)
|
|
----
|
|
|
|
[#spritescale]
|
|
==== spriteScale
|
|
|
|
[source,text]
|
|
----
|
|
spriteScale(id, scale)
|
|
spriteScale(id, scaleX, scaleY)
|
|
----
|
|
|
|
Scales the sprite about its center, uniformly or per axis, keeping the current rotation. Factors must be greater than `0`; `1.0` is natural size, and a factor of exactly `1.0` on both axes with no rotation draws the source image directly. The scaled image is rebuilt only when a factor changes, using the sampling chosen by `spriteQuality`. Prefer the stretched form of `spriteDraw` when the target size changes every frame, since it does not rebuild anything.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- sprite handle.
|
|
* `scale` -- uniform factor.
|
|
* `scaleX`, `scaleY` -- separate horizontal and vertical factors.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritequality,spriteQuality>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Enlarge the highlighted menu entry; the rebuild happens only when the selection moves.
|
|
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
|
|
----
|
|
|
|
[#spritesetframe]
|
|
==== spriteSetFrame
|
|
|
|
[source,text]
|
|
----
|
|
spriteSetFrame(id, frame)
|
|
----
|
|
|
|
Jumps an animation to the given frame and restarts that frame's delay timer, so the frame is shown for its full duration before the next one. Playback state is not changed: a paused sprite stays paused on the new frame and a playing one continues from it. The call is ignored for still images, for frames outside `0` to the last frame, and when the sprite already shows that frame.
|
|
|
|
*Notes:* Frames are numbered from `0` here; `spriteDrawFrame`, the Hypseus extension, numbers them from `1`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritegetframe,spriteGetFrame>>, <<spriteplay,spritePlay>>, <<spritedrawframe,spriteDrawFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Show the door in the state the game logic says it is in.
|
|
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
|
|
----
|
|
|
|
[#spriteunload]
|
|
==== spriteUnload
|
|
|
|
[source,text]
|
|
----
|
|
spriteUnload(id)
|
|
----
|
|
|
|
Frees the sprite: the source image, the transformed copy if one exists, and all animation frames. The handle is invalid afterward and any further use ends the script with an error. Sprites made by `fontToSprite` are released the same way. Sprites still loaded when the game ends are freed by the engine, but games that create sprites during play (text re-rendered as scores change, for instance) must unload the old ones or memory grows without bound.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<spriteload,spriteLoad>>, <<fonttosprite,fontToSprite>>, <<onshutdown,onShutdown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Re-render the score only when it changes, releasing the previous sprite.
|
|
function updateScoreSprite()
|
|
if scoreSprite ~= nil then
|
|
spriteUnload(scoreSprite)
|
|
end
|
|
scoreSprite = fontToSprite("SCORE " .. score)
|
|
end
|
|
|
|
function onShutdown()
|
|
spriteUnload(scoreSprite)
|
|
spriteUnload(cabinetSprite)
|
|
end
|
|
----
|
|
|
|
[#subtitleref]
|
|
=== Subtitle
|
|
|
|
SubRip subtitles over the disc picture. A game loads a `.srt` with <<srtload,`srtLoad`>>, switches it on with <<srtenable,`srtEnable`>>, and the engine puts each cue up and takes it down again as the disc reaches it. See <<subtitles,Subtitles>> for the file format, the timing and how to restyle them.
|
|
|
|
The subtitles and <<overlaybanner,`overlayBanner`>> share one shipped document, `Singe/subtitle.rml`, styled by `Singe/subtitle.rcss`; a game or a theme may replace either. Both are drawn over the picture, above the overlay and below the screen space GUIs, and they take no mouse input.
|
|
|
|
[#srtclear]
|
|
==== srtClear
|
|
|
|
[source,text]
|
|
----
|
|
srtClear()
|
|
----
|
|
|
|
Hypseus extension. Takes whatever is on screen off at once -- a cue or a banner -- and leaves the loaded cues and the enabled state alone. The cue that was showing stays off until the disc reaches the next one; nothing has to be reloaded and nothing has to be switched back on.
|
|
|
|
*Parameters:* none.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<srtenable,srtEnable>>, <<overlaybanner,overlayBanner>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A cutscene ends early; drop the line that was still up.
|
|
srtClear()
|
|
----
|
|
|
|
[#srtenable]
|
|
==== srtEnable
|
|
|
|
[source,text]
|
|
----
|
|
srtEnable(enabled)
|
|
----
|
|
|
|
Hypseus extension. Whether the loaded cues are shown as the disc plays. Switching it off takes any cue that is showing off with it; switching it on again puts up whatever cue covers the frame the disc is on. A script starts with subtitles off, so a game that loads them must switch them on.
|
|
|
|
A non-boolean argument terminates the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<srtload,srtLoad>>, <<srtclear,srtClear>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
srtEnable(SETTINGS.subtitles)
|
|
----
|
|
|
|
[#srtload]
|
|
==== srtLoad
|
|
|
|
[source,text]
|
|
----
|
|
loaded = srtLoad(filename)
|
|
----
|
|
|
|
Hypseus extension. Reads a SubRip file through the virtual file system and turns its timestamps into disc frames at the disc's own frame rate. Loading replaces whatever was loaded before; <<srtenable,`srtEnable`>> then shows it.
|
|
|
|
It answers `false`, having loaded nothing, when the file cannot be read, when it holds no cue that parses, or when there is no disc to time it against -- so a malformed file is a `false`, not a crash. A warning naming the file is printed in every one of those cases. Cues that do not parse inside an otherwise good file are skipped, as Hypseus skips them.
|
|
|
|
Because the timestamps become frame numbers when the file is loaded, and the frame number is what the engine matches, a cue is found again wherever the disc is sent: a search, a skip or a step lands on the right line at once.
|
|
|
|
*Parameters:*
|
|
|
|
* `filename` -- string; a path the virtual file system can reach.
|
|
|
|
*Returns:* boolean, whether any cue was loaded.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<srtenable,srtEnable>>, <<srtposition,srtPosition>>, <<subtitles,Subtitles>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
if srtLoad("subs/" .. language .. ".srt") then
|
|
srtEnable(true)
|
|
end
|
|
----
|
|
|
|
[#srtposition]
|
|
==== srtLoadTrack
|
|
|
|
[source,text]
|
|
----
|
|
loaded = srtLoadTrack(track)
|
|
----
|
|
|
|
The same as <<srtload,`srtLoad`>>, except that the subtitles come from inside the disc's own container rather than from a `.srt` file beside it. Everything after loading is identical: the cues become disc frame numbers at the disc's frame rate, so they are found again after any seek, and <<srtenable,`srtEnable`>>, <<srtposition,`srtPosition`>> and <<srtclear,`srtClear`>> work the same.
|
|
|
|
The whole file is walked once to collect the track, decoding no pictures, so a long disc takes a moment -- do it while something else is on screen rather than between two frames of gameplay.
|
|
|
|
A track that holds pictures rather than words (VobSub or PGS) cannot be read: it answers `false`, as an unreadable `.srt` does, and leaves the subtitles cleared. So does a track number that names nothing, and so does a game with no disc.
|
|
|
|
*Parameters:*
|
|
|
|
* `track` -- number; `0` to `discGetSubtitleTracks() - 1`.
|
|
|
|
*Returns:* `true` when at least one cue was read.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<srtload,srtLoad>>, <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtenable,srtEnable>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Subtitles the rip carried, with a .srt beside the game as the fallback.
|
|
if discGetSubtitleTracks() > 0 then
|
|
srtLoadTrack(0)
|
|
else
|
|
srtLoad(DIR .. "subtitles.srt")
|
|
end
|
|
srtEnable(true)
|
|
----
|
|
|
|
==== srtPosition
|
|
|
|
[source,text]
|
|
----
|
|
srtPosition(percent)
|
|
----
|
|
|
|
Hypseus extension. Where a subtitle sits, as a per cent down the picture, from 1 to 95. The default is 80. A number outside that range is ignored, as Hypseus ignores it, rather than being clamped or refused.
|
|
|
|
The height applies to the cue on screen straight away and to every cue after it. <<overlaybanner,`overlayBanner`>> carries its own height and is not affected.
|
|
|
|
*Parameters:*
|
|
|
|
* `percent` -- number; 1 to 95.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<srtload,srtLoad>>, <<subtitles,Subtitles>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Keep the lines clear of the scoreboard along the bottom.
|
|
srtPosition(70)
|
|
----
|
|
|
|
[#onlinescores]
|
|
=== Online high scores
|
|
|
|
A game posts a score in one line:
|
|
|
|
[source,lua]
|
|
----
|
|
scoreSubmit(playerScore)
|
|
----
|
|
|
|
Everything else was set up once, in the menu: which service, which account, and
|
|
what name the player appears under (see
|
|
<<onlineservice,Online: accounts, games and scores>>). A game needs only a
|
|
`GAME_ID` in its `games.dat` entry -- a UUID, which is what the board is kept under. See
|
|
<<singegetgameid,singeGetGameId>> for the format.
|
|
|
|
|===
|
|
| Call | What it does
|
|
|
|
| `scoreBegin()`
|
|
| Say a play is starting. Optional, and worth calling: see below.
|
|
|
|
| `scoreSubmit(value [, board [, meta]])`
|
|
| Queue a score. Returns `false` if the cabinet is not signed in or the game has no `GAME_ID`.
|
|
|
|
| `scoreBoard(onDone [, board])`
|
|
| Fetch the board. `onDone(ok, result)` with `result.top` (a list of `{name, value}`) and
|
|
`result.standing` (this player's `value`, `rank` and the number of `players`).
|
|
|
|
| `scorePlayerName()`
|
|
| The name this cabinet posts under, or `nil` if none has been chosen.
|
|
|
|
| `scoreWaiting()`
|
|
| How many scores are queued and not yet sent.
|
|
|
|
| `scoreUpdate()`
|
|
| Once a frame, from `onOverlayUpdate`. Nothing progresses without it.
|
|
|===
|
|
|
|
**A score is queued, not sent.** It is written to disk and handed to the service
|
|
when there is a connection, because a cabinet is often offline and a game must
|
|
never stall on the wire to show its own board. A queued score survives the
|
|
machine being switched off.
|
|
|
|
**A board can have more than one.** Pass a board name to keep a time attack
|
|
separate from a points total; the default is `default`.
|
|
|
|
**The name on a board is a handle, never an address.** It is chosen in the
|
|
Online Account tool, kept apart from the account, and is the only thing about a
|
|
player that other players see. Three to twenty characters of letters, digits,
|
|
hyphens and underscores, starting and ending with a letter or digit -- no
|
|
spaces, no dots, and no `@`, so a handle cannot even look like an email address.
|
|
Case is ignored for uniqueness, so `Ann` and `ANN` are one name.
|
|
|
|
**A handle is chosen once.** Renaming freely would mean a leaderboard's history
|
|
is not really anyone's. An operator can change one; a player cannot. An email
|
|
address cannot be changed at all.
|
|
|
|
Handles are checked against a list the service's operator maintains: identities
|
|
nobody should claim (`admin`, `staff`) and words that have no place on a screen
|
|
in a public room. A score is refused until a name has been chosen, because a
|
|
score with nobody's name on it is not on a board in any useful sense.
|
|
|
|
**What a score means.** The cabinet is the player's machine, so a submitted
|
|
score is a *claim* -- nothing the client can do makes it more than that, and
|
|
signing it with a secret shipped in the client would only look like security.
|
|
The service records who claimed what, rate limits submissions, and flags scores
|
|
that are wildly out of line for a person to look at. It does not pretend to
|
|
verify them.
|
|
|
|
**`scoreBegin()` is worth calling** for the one thing that is not a claim. Call
|
|
it when a play starts and the service notes the moment on its own clock; the
|
|
score that follows carries how long the play took, measured server-side. That
|
|
number has not been through the player's machine, which is what makes it worth
|
|
having -- a large score arriving seconds after the game began is visible without
|
|
anyone being accused of anything.
|
|
|
|
Nothing is rejected on it. For a game with a disc the video length is a floor
|
|
nobody can beat, but a disc is optional, and a threshold learned from
|
|
submissions is set by whoever submits first. So the time is recorded, shown to
|
|
the operator beside the score, and the judgement is left to a person. A cabinet
|
|
that is offline when a play starts simply records no time, which is honest
|
|
rather than broken.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function startLevel()
|
|
scoreBegin() -- the service starts its clock
|
|
...
|
|
end
|
|
----
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- At the end of a game, post the score and then show where it landed.
|
|
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]
|
|
=== Stats
|
|
|
|
The developer's overlay, in the top left corner: how long the frame took and its
|
|
worst over the last two seconds, the frame rate, Lua's memory, how many sprites,
|
|
sounds and timers are alive, how many of the scene's nodes were drawn and in how
|
|
many batches, how much texture memory it is holding, and what the disc is doing.
|
|
|
|
It is drawn with SDL's own debug font, so it needs no font file, no asset and no
|
|
GUI document, and it appears even on a machine where the 3D device never came
|
|
up. It is drawn after <<singescreenshot,`singeScreenshot`>> takes its picture,
|
|
so it never lands in a screenshot.
|
|
|
|
Leave it off in a shipped game. Nothing stops you putting it behind a key in
|
|
your own service menu.
|
|
|
|
[#statsenable]
|
|
==== statsEnable
|
|
|
|
[source,text]
|
|
----
|
|
statsEnable(enabled)
|
|
----
|
|
|
|
Shows or hides the overlay. While it is shown the picture is redrawn every frame, so the numbers keep moving even when nothing else on screen does.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<statsisenabled,statsIsEnabled>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A developer key, behind a flag so the shipped game has no such key.
|
|
function onInputPressed(key)
|
|
if DEVELOPER and key == SWITCH_SERVICE then
|
|
statsEnable(not statsIsEnabled())
|
|
end
|
|
end
|
|
----
|
|
|
|
==== statsIsEnabled
|
|
|
|
[source,text]
|
|
----
|
|
shown = statsIsEnabled()
|
|
----
|
|
|
|
Whether the overlay is on.
|
|
|
|
*Parameters:*
|
|
|
|
* None.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
[#terrain]
|
|
=== Terrain
|
|
|
|
Heightmap ground made with `meshHeightmap`, which turns a grayscale image into a mesh one vertex per pixel; `terrainGetHeight` reads that mesh back through the node carrying it. Positions and heights are in world units, and the node's position and scale are taken into account. See Terrain in <<scenes3d,3D Scenes>>.
|
|
|
|
[#terraingetheight]
|
|
==== terrainGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = terrainGetHeight(node, x, z)
|
|
----
|
|
|
|
The world height of the heightmap mesh on `node` at world `x, z`, interpolated between the nearest four samples, or `nil` beyond its edges or when the node's mesh is not a heightmap. The node's world position and scale count; its rotation is ignored, since terrains lie flat. Use it for placing trees, spawning things on the ground, or a camera that follows the surface, without the cost of a ray cast. Raises an error for a node handle that is not valid.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the node whose mesh came from `meshHeightmap`.
|
|
* `x, z` -- the world position to sample.
|
|
|
|
*Returns:* A number, or `nil` off the terrain.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<meshheightmap,meshHeightmap>>, <<physicsraycast,physicsRaycast>>, <<nodesetposition,nodeSetPosition>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Trees on the hills, and a camera that never dips below the ground.
|
|
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
|
|
|
|
Something to happen later, without counting frames yourself. Timers run on the
|
|
engine's clock, which is the virtual one under `--deterministic`, so a timed
|
|
game repeats exactly between runs. They fire with the rest of the frame's
|
|
callbacks, on the same thread as everything else -- there is nothing to lock and
|
|
nothing that can fire while your script is halfway through something.
|
|
|
|
A timer that has fallen behind -- a long load, a breakpoint -- fires once when
|
|
the game catches up rather than firing the twenty times it missed.
|
|
|
|
Everything is forgotten when the script reloads, so a timer from the last run
|
|
never fires into the next one.
|
|
|
|
==== timerAfter
|
|
|
|
[source,text]
|
|
----
|
|
id = timerAfter(milliseconds, function)
|
|
----
|
|
|
|
Calls the function once, later. The function is given the timer's own id, so one function can serve several timers.
|
|
|
|
*Parameters:*
|
|
|
|
* `milliseconds` -- number; zero or less fires on the next frame.
|
|
* `function` -- called as `function(id)`.
|
|
|
|
*Returns:* a number; the timer's id.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<timercancel,timerCancel>>, <<timerevery,timerEvery>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Let the explosion finish before the game over screen.
|
|
soundPlay(bang)
|
|
timerAfter(1500, function()
|
|
showGameOver()
|
|
end)
|
|
----
|
|
|
|
==== timerCancel
|
|
|
|
[source,text]
|
|
----
|
|
timerCancel(id)
|
|
----
|
|
|
|
Stops a timer. Cancelling one that has already fired is not an error, so nothing has to check first.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; from `timerAfter` or `timerEvery`.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== timerEvery
|
|
|
|
[source,text]
|
|
----
|
|
id = timerEvery(milliseconds, function)
|
|
----
|
|
|
|
Calls the function over and over until it is cancelled.
|
|
|
|
*Parameters:*
|
|
|
|
* `milliseconds` -- number; how long between calls.
|
|
* `function` -- called as `function(id)`.
|
|
|
|
*Returns:* a number; the timer's id.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<timercancel,timerCancel>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Tick the countdown once a second, and stop it when it runs out.
|
|
local left = 30
|
|
local clock
|
|
clock = timerEvery(1000, function()
|
|
left = left - 1
|
|
if left <= 0 then
|
|
timerCancel(clock)
|
|
outOfTime()
|
|
end
|
|
end)
|
|
----
|
|
|
|
==== timerIsActive
|
|
|
|
[source,text]
|
|
----
|
|
running = timerIsActive(id)
|
|
----
|
|
|
|
Whether the timer will fire again: `true` for a repeating one until it is cancelled, `false` for a one shot that has already gone off.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
[#tween]
|
|
=== Tween
|
|
|
|
Moving a number from one value to another over time, on a curve. Fades, slides,
|
|
camera moves, a health bar draining, a menu sliding in -- all the same thing.
|
|
|
|
`tweenValue` hands you the number and lets you decide what it means. That is
|
|
deliberate: the engine has nodes, sprites, GUI elements, materials and lights,
|
|
and a tween that knew about each of them would be five sets of calls to keep in
|
|
step. One that hands over a number is none, and it works on things the engine
|
|
has never heard of.
|
|
|
|
The easings are the usual ones. `EASE_LINEAR` is no curve at all;
|
|
`EASE_QUAD_*`, `EASE_CUBIC_*`, `EASE_SINE_*` and `EASE_EXPO_*` run from gentle
|
|
to sharp; `EASE_BACK_*` overshoots and comes back; `EASE_ELASTIC_*` springs;
|
|
`EASE_BOUNCE_*` drops and bounces. Each comes in `_IN` (slow at the start),
|
|
`_OUT` (slow at the end) and `_IN_OUT` (both).
|
|
|
|
==== tweenCancel
|
|
|
|
[source,text]
|
|
----
|
|
tweenCancel(id)
|
|
----
|
|
|
|
Stops a tween where it is. Neither function is called again, and the value stays wherever it had reached.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number; from `tweenValue`.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== tweenIsActive
|
|
|
|
[source,text]
|
|
----
|
|
running = tweenIsActive(id)
|
|
----
|
|
|
|
Whether the tween is still running.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- number.
|
|
|
|
*Returns:* `true` or `false`.
|
|
|
|
*Since:* 3.00.
|
|
|
|
==== tweenValue
|
|
|
|
[source,text]
|
|
----
|
|
id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
|
|
----
|
|
|
|
Moves a number from `from` to `to` over `milliseconds`, calling `onUpdate(value, progress)` every frame -- `progress` runs 0 to 1 and ignores the curve -- and `onDone()` once at the end. The last `onUpdate` always lands exactly on `to`, so nothing finishes a pixel short.
|
|
|
|
*Parameters:*
|
|
|
|
* `from`, `to` -- numbers.
|
|
* `milliseconds` -- number; zero or less arrives on the next frame.
|
|
* `easing` -- one of the `EASE_` values.
|
|
* `onUpdate` -- called as `function(value, progress)`.
|
|
* `onDone` -- optional; called with no arguments when it finishes.
|
|
|
|
*Returns:* a number; the tween's id.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<tweencancel,tweenCancel>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Slide the title in and fade the overlay up at the same time.
|
|
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]
|
|
=== Vehicle
|
|
|
|
Cars, motorcycles, tanks and boats built on a chassis body (see <<physics,Physics>>, Vehicles and Water). A vehicle has no handle of its own: it is addressed by its chassis node, which must already carry a `BODY_DYNAMIC` body from `bodyNew`, and a node without a vehicle terminates the script in every call after `vehicleNew`. The chassis faces -Z with Y up; distances are world units, forces newtons, torques newton meters and engine speeds revolutions a minute. Every tuning call marks the vehicle for a rebuild at the next physics step, around the wheel positions recorded when the wheels were added. Up to 16 vehicles at once, each with up to 16 wheels.
|
|
|
|
[#vehiclenew]
|
|
==== vehicleNew
|
|
|
|
[source,text]
|
|
----
|
|
vehicleNew(node, kind)
|
|
----
|
|
|
|
Turns the dynamic body on `node` into the chassis of a vehicle. A car, motorcycle or tank needs at least two wheels from `vehicleAddWheel` before the next physics step builds it; a boat has no wheels and is pushed by a propeller that only works under a surface made with `bodySetWater`. The new vehicle starts from the defaults: a 500 Nm engine peaking at 6000 rpm and idling at 1000, Jolt's five-speed automatic gearbox, 35 degrees of steering lock, 1500 Nm brakes and a 4000 Nm hand brake, 1.5 Hz suspension at 0.5 damping and no anti-roll bars. Calling it again on the same node discards the old vehicle and its wheels first. Raises an error for an unknown kind, a node whose body is not dynamic, a wheeled kind in a 2D world, or when 16 vehicles already exist.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node, carrying a `BODY_DYNAMIC` body.
|
|
* `kind` -- `VEHICLE_CAR` (any number of axles), `VEHICLE_MOTORCYCLE` (two wheels in line, leaning through turns), `VEHICLE_TANK` (wheels on two tracks, turning on the spot) or `VEHICLE_BOAT` (a buoyant hull with a propeller and a rudder).
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehicledrive,vehicleDrive>>, <<vehiclesetthrust,vehicleSetThrust>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Removes the vehicle from its chassis: the wheel constraint, the driver input and all the tuning. The body stays an ordinary dynamic body, so a wreck keeps sliding and tumbling, and the wheel nodes stay where the last step posed them; nothing moves them again unless the script does. A node without a vehicle terminates the script.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclenew,vehicleNew>>, <<bodydelete,bodyDelete>>
|
|
|
|
.Example
|
|
[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) -- The chassis tumbles on as a body
|
|
soundPlay(crashSound)
|
|
emitterStart(smoke)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vehicleaddwheel]
|
|
==== vehicleAddWheel
|
|
|
|
[source,text]
|
|
----
|
|
index = vehicleAddWheel(node, wheelNode, radius, width, suspension)
|
|
----
|
|
|
|
Adds a wheel to the vehicle where `wheelNode` stands relative to the chassis when the vehicle is next built. The suspension hangs the wheel `suspension` units below that point when unloaded and compresses to half that. From then on the engine writes the wheel node's world transform after every step, with X as the axle and Y up, so the visible tire belongs on a child node: a `meshCylinder` turned 90 degrees about Z. Wheels ahead of the average wheel position along the nose count as front wheels: they steer by default and carry no hand brake. A tank sorts its wheels onto the left or right track by which side of the chassis they sit on and needs at least one on each side. Values below `0.001` are raised to it. Raises an error at the 17th wheel.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `wheelNode` -- the node the wheel hangs from; its position relative to the chassis is the attachment point, and the engine poses it from now on.
|
|
* `radius` -- the wheel's radius, in world units.
|
|
* `width` -- the wheel's width, in world units.
|
|
* `suspension` -- the suspension's length at full extension, in world units; it compresses to half.
|
|
|
|
*Returns:* The wheel's index, from `0`, for `vehicleSetWheel`, `vehicleIsWheelOnGround` and `vehicleGetWheelSlip`.
|
|
|
|
*Notes:* A vehicle is only built once it has two wheels, and a car or motorcycle also needs a driven wheel; until then it sits as a plain body and the drive calls do nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehiclesetsuspension,vehicleSetSuspension>>, <<nodefind,nodeFind>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The truck model carries empty nodes named for its wheels.
|
|
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)
|
|
----
|
|
|
|
Sets a boat's propeller: the force it pushes with at full throttle, default `2000` newtons, and the point in the hull's frame it pushes at, default `0, -0.2, 1`, just below the deck at the stern. The push is along the hull's nose and only happens while that point is under a water surface; the same point decides whether the rudder and the brake act. A negative force becomes `0`. Wheeled kinds store the values and ignore them.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the hull node.
|
|
* `maxForce` -- the push at full throttle, in newtons.
|
|
* `x`, `y`, `z` -- where the propeller sits, in the hull's frame; the stern is +Z.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetrudder,vehicleSetRudder>>, <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
|
|
|
|
.Example
|
|
[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) -- Rides high on the pool
|
|
vehicleNew(raft, VEHICLE_BOAT)
|
|
vehicleSetThrust(raft, 600, 0, -0.1, 1.2) -- An outboard hung off the stern
|
|
vehicleSetRudder(raft, 300)
|
|
----
|
|
|
|
[#vehiclesetwheel]
|
|
==== vehicleSetWheel
|
|
|
|
[source,text]
|
|
----
|
|
vehicleSetWheel(node, index, steered, driven)
|
|
----
|
|
|
|
Says whether one wheel turns with the steering input and whether the engine drives it, replacing the defaults for that wheel: front wheels steer, and every wheel drives except a motorcycle's front one. Driven wheels pair up across the chassis into differentials, one per axle, and the engine's torque is shared equally between the axles; a driven wheel with no partner is an axle of its own. A tank ignores both flags, since its tracks drive every wheel and turn the hull. Takes effect at the next step's rebuild. Raises an error for an index the vehicle has no wheel for.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `index` -- the wheel's index from `vehicleAddWheel`.
|
|
* `steered` -- `true` for the wheel to turn up to `vehicleSetSteering` at full lock.
|
|
* `driven` -- `true` for the engine to turn it.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetsteering,vehicleSetSteering>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A rear-wheel drive sports car: the front wheels only steer.
|
|
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)
|
|
----
|
|
|
|
Sets the engine: peak torque in newton meters, the redline and the idle speed in revolutions a minute. Defaults are `500`, `6000` and `1000`, and the idle is `1000` again when it is left out. The torque is kept at `1` or more, the redline at `100` or more and the idle between `1` and the redline. A boat has no engine and ignores the call. Takes effect at the next step's rebuild.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `maxTorque` -- peak torque, in newton meters.
|
|
* `maxRpm` -- the redline, in revolutions a minute.
|
|
* `minRpm` -- the idle speed, in revolutions a minute; default `1000`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
vehicleSetEngine(tank, 1500, 4000, 800) -- A diesel: torque low down
|
|
vehicleSetEngine(bike, 150, 9000, 1200) -- A screamer
|
|
|
|
function onOverlayUpdate()
|
|
local rpm = vehicleGetRpm(bike)
|
|
overlayClear()
|
|
fontPrint(20, 20, string.format("%5.0f rpm", rpm))
|
|
if rpm > 8500 then
|
|
spriteDraw(shiftLight, 200, 20) -- Time to change up
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#vehiclesetgears]
|
|
==== vehicleSetGears
|
|
|
|
[source,text]
|
|
----
|
|
vehicleSetGears(node, { ratio, ... })
|
|
vehicleSetGears(node, { ratio, ... }, reverseRatio)
|
|
vehicleSetGears(node, { ratio, ... }, reverseRatio, automatic)
|
|
----
|
|
|
|
Sets the gearbox: one to eight forward ratios, first gear first, the reverse ratio (default `2.9`, given as a positive number) and whether the box shifts by itself (default `true`). A vehicle that never gets this call runs Jolt's five-speed, `2.66`, `1.78`, `1.3`, `1.0` and `0.74`, with the same reverse. With `automatic` false the box no longer shifts on its own, and there is no call to shift it from script. A boat has no gearbox and ignores the call. Takes effect at the next step's rebuild. Raises an error when the table is empty or holds more than eight ratios.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `{ ratio, ... }` -- forward gear ratios, engine turns per gearbox turn, first gear first; one to eight of them.
|
|
* `reverseRatio` -- the reverse gear's ratio; default `2.9`.
|
|
* `automatic` -- `true` (default) for the box to shift by itself.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclegetgear,vehicleGetGear>>, <<vehiclesetengine,vehicleSetEngine>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A close-ratio six-speed for the race car, a tall three-speed for the 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)
|
|
----
|
|
|
|
Sets a boat's rudder: the torque about the hull's up axis at full steering input, default `800` newton meters. The rudder bites with speed, a fifth of the torque at rest and all of it from 3 world units a second, and it turns the other way while the boat moves astern. It only acts while the propeller point is under water. A negative torque becomes `0`; wheeled kinds ignore the call.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetthrust,vehicleSetThrust>>, <<vehicledrive,vehicleDrive>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
vehicleNew(patrolBoat, VEHICLE_BOAT)
|
|
vehicleSetThrust(patrolBoat, 4000, 0, -0.4, 3.5)
|
|
vehicleSetRudder(patrolBoat, 2500) -- Heavy hull, big rudder
|
|
|
|
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)
|
|
----
|
|
|
|
Sets every wheel's spring: its natural frequency in hertz (default `1.5`, kept at `0.1` or more; stiffer is higher) and its damping ratio (default `0.5`; `0` bounces, `1` settles without overshoot). Takes effect at the next step's rebuild, on every wheel at once; there is no per-wheel spring. A boat has no suspension and ignores the call.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `frequency` -- the spring's natural frequency, in hertz.
|
|
* `damping` -- the damping ratio, `0` upward.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The buggy floats over the dunes; the race car barely moves on its springs.
|
|
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)
|
|
----
|
|
|
|
Sets how far the steered wheels turn at full lock, in degrees; default `35`, clamped to `0` through `89`. A steering input of `-1` or `1` from `vehicleDrive` reaches this angle, and smaller inputs a proportion of it. Takes effect at the next step's rebuild. A tank turns by its tracks and a boat by its rudder, so both ignore the call.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehicledrive,vehicleDrive>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The go-kart turns on a dime; the truck needs a wide arc.
|
|
vehicleSetSteering(kart, 45)
|
|
vehicleSetSteering(truck, 25)
|
|
vehicleSetSteering(bus, 20)
|
|
if difficulty == "arcade" then
|
|
vehicleSetSteering(truck, 35) -- Forgiving handling
|
|
end
|
|
----
|
|
|
|
[#vehiclesetbrakes]
|
|
==== vehicleSetBrakes
|
|
|
|
[source,text]
|
|
----
|
|
vehicleSetBrakes(node, brakeTorque, handBrakeTorque)
|
|
----
|
|
|
|
Sets the brake torque every wheel can apply at full pedal, default `1500` newton meters, and the hand brake torque on the rear wheels, default `4000`; the front wheels, those ahead of the average wheel position, have no hand brake. Negative values become `0`. A tank brakes its tracks with Jolt's track defaults and ignores the call, and a boat brakes by dragging against its own velocity, so it ignores the call too. Takes effect at the next step's rebuild.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `brakeTorque` -- foot brake torque on every wheel, in newton meters.
|
|
* `handBrakeTorque` -- hand brake torque on the rear wheels, in newton meters.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehicledrive,vehicleDrive>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
vehicleSetBrakes(car, 2200, 6000) -- Race pads and a drift-ready hand brake
|
|
|
|
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)
|
|
----
|
|
|
|
Adds an anti-roll bar across each axle, in newtons a meter; `0` (default) fits none and a negative value becomes `0`. A bar joins the two driven wheels of an axle, so an axle whose wheels do not drive gets no bar, and a motorcycle has none. A few hundred keeps a car flat in corners; too much lifts the inside wheels. A tank ignores the call. Takes effect at the next step's rebuild.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetsuspension,vehicleSetSuspension>>, <<vehiclesetwheel,vehicleSetWheel>>
|
|
|
|
.Example
|
|
[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) -- Stays flat through the chicane
|
|
----
|
|
|
|
[#vehicledrive]
|
|
==== vehicleDrive
|
|
|
|
[source,text]
|
|
----
|
|
vehicleDrive(node, forward, right)
|
|
vehicleDrive(node, forward, right, brake)
|
|
vehicleDrive(node, forward, right, brake, handBrake)
|
|
----
|
|
|
|
Sets the driver's input, which holds until the next call, so call it every frame from `onOverlayUpdate`. Throttle and steering run `-1` to `1`, brake and hand brake `0` to `1`; values outside are clamped and inputs left out are `0`. A car or motorcycle gets all four as Jolt's driver input, a negative throttle reversing through the reverse gear. A tank takes `forward` as its throttle and `right` slows the inside track; with no throttle the tracks run against each other so the hull pivots on the spot, and the hand brake is ignored. A boat takes `forward` as the propeller's throttle, `right` as the rudder and `brake` as a drag against the hull's velocity, ignores the hand brake, and does nothing at all while the propeller is out of the water. Any non-zero input wakes a chassis that has gone to sleep.
|
|
|
|
*Parameters:*
|
|
|
|
* `node` -- the chassis node.
|
|
* `forward` -- throttle, `-1` to `1`; negative reverses.
|
|
* `right` -- steering, `-1` (left) to `1` (right).
|
|
* `brake` -- foot brake, `0` to `1`; default `0`.
|
|
* `handBrake` -- hand brake, `0` to `1`; default `0`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclegetspeed,vehicleGetSpeed>>, <<vehiclesetbrakes,vehicleSetBrakes>>, <<controllergetaxis,controllerGetAxis>>
|
|
|
|
.Example
|
|
[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 -- Reverse while A is held
|
|
end
|
|
vehicleDrive(car, gas, steer, brake, handBrake)
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#vehiclegetspeed]
|
|
==== vehicleGetSpeed
|
|
|
|
[source,text]
|
|
----
|
|
speed = vehicleGetSpeed(node)
|
|
----
|
|
|
|
The chassis body's velocity along its nose, in world units a second, negative while it moves astern; sideways motion in a drift does not count. Works for every kind, boats included, from the moment `vehicleNew` is called.
|
|
|
|
*Returns:* A number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclegetrpm,vehicleGetRpm>>, <<vehiclegetgear,vehicleGetGear>>, <<bodygetvelocity,bodyGetVelocity>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
The engine's speed in revolutions a minute, between the idle and the redline from `vehicleSetEngine`, for a rev counter or the pitch of an engine loop. `0` for a boat, which has no engine, and for a vehicle that has not been built yet because it lacks wheels.
|
|
|
|
*Returns:* A number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetengine,vehicleSetEngine>>, <<vehiclegetgear,vehicleGetGear>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A rev counter bar that turns red past 6000.
|
|
function onOverlayUpdate()
|
|
local rpm = vehicleGetRpm(car)
|
|
overlayClear()
|
|
if rpm > 6000 then
|
|
colorForeground(255, 40, 40)
|
|
else
|
|
colorForeground(60, 220, 60)
|
|
end
|
|
overlayBox(20, 20, 20 + math.floor(rpm / 6500 * 200), 32)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#vehiclegetgear]
|
|
==== vehicleGetGear
|
|
|
|
[source,text]
|
|
----
|
|
gear = vehicleGetGear(node)
|
|
----
|
|
|
|
The gear the box has engaged: `0` in neutral, `-1` in reverse, `1` upward for the forward gears in the order `vehicleSetGears` gave them. `0` for a boat and for a vehicle that has not been built yet.
|
|
|
|
*Returns:* An integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
|
|
|
|
.Example
|
|
[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)
|
|
----
|
|
|
|
Whether that wheel's suspension cast found anything to press against this step. `false` for an index the vehicle has no wheel for and for a vehicle that has not been built yet; no error is raised for either.
|
|
|
|
*Returns:* True or false.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Airborne when no wheel touches; a landing after a jump shakes the camera.
|
|
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)
|
|
----
|
|
|
|
How much the wheel spins or skids along its rolling direction, `0` gripping to `1` spinning or locked, for tire squeal, smoke and skid marks. Always `0` for a tank's wheels, for an index the vehicle has no wheel for, and for a vehicle that has not been built yet.
|
|
|
|
*Returns:* A number.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vehicleiswheelonground,vehicleIsWheelOnGround>>, <<vehiclegetspeed,vehicleGetSpeed>>
|
|
|
|
.Example
|
|
[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]
|
|
=== View
|
|
|
|
A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows (a directional light's cascades included: each point takes the finest cascade that covers it) and no bloom, so keep them few and small. Every view turns billboards (`nodeSetBillboard`) to its own camera and sorts blended draws back to front from it, so a view whose camera looks from elsewhere still sees billboards face on and transparent objects in the right order. A bad handle raises an error. See <<scenes3d,3D Scenes>>.
|
|
|
|
[#viewdelete]
|
|
==== viewDelete
|
|
|
|
[source,text]
|
|
----
|
|
viewDelete(view)
|
|
----
|
|
|
|
Frees the view and its textures, and stops the extra render pass it cost. Materials showing it go back to their own texture. The handle is invalid afterwards and raises an error if used again; since only four views exist at once, delete the ones a level no longer needs.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<viewnew,viewNew>>, <<materialsetview,materialSetView>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The security room's monitors only render while the player is in it.
|
|
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)
|
|
----
|
|
|
|
Makes a camera rendered to a `width` by `height` texture every frame (each clamped to `1` to `4096`), from the default view until `viewSetCamera` names a node, and returns the view handle for `materialSetView`. The view uses the projection from `cameraSetPerspective` or `cameraSetOrthographic` at its own aspect ratio. The scene renders once more per view, so `512` by `384` is plenty for a monitor and a Raspberry Pi wants smaller. Raises an error when 3D is unavailable or four views are already in use.
|
|
|
|
*Parameters:*
|
|
|
|
* `width` -- the texture's width in pixels.
|
|
* `height` -- the texture's height in pixels.
|
|
|
|
*Returns:* The view handle, an integer.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<viewsetcamera,viewSetCamera>>, <<materialsetview,materialSetView>>, <<viewdelete,viewDelete>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A security monitor showing a camera behind the player.
|
|
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)
|
|
----
|
|
|
|
The node the view looks from, down its own -Z like the main camera, so its parents' movement counts. With `nil` or no second argument the view goes back to the default view from `(0, 0, 5)` looking at the origin. Parent the node to a car for a rear-view mirror, or reflect the main camera's position about a wall for a mirror in it. Raises an error for a node handle that is not valid.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<viewnew,viewNew>>, <<cameraset,cameraSet>>, <<nodelookat,nodeLookAt>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The lobby monitor cycles through three security cameras.
|
|
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
|
|
|
|
The `video*` family handles additional video clips on top of the disc: a character talking in a corner, a pre-rendered animation at a story beat, several concurrent cues. Any number of videos can be loaded with `videoLoad`, and each returns an opaque integer handle that every other `video*` function takes as its first argument; a handle that has been unloaded terminates the script. Videos are drawn onto the overlay with `videoDraw` in overlay coordinates and composite like sprites, and a video advances only while it is being drawn (or used as a material through <<materialsetvideo,materialSetVideo>>). Each video has its own audio track selection and stereo volume, independent of the disc. Like sprites, videos must be drawn from `onOverlayUpdate` and eventually freed with `videoUnload`; the drawing rules are in <<overlay,Overlay>>.
|
|
|
|
[#videodraw]
|
|
==== videoDraw
|
|
|
|
[source,text]
|
|
----
|
|
videoDraw(id, x, y, x2, y2)
|
|
videoDraw(id, x, y, centered)
|
|
----
|
|
|
|
Advances a video to the frame its clock calls for and draws that frame on the overlay. The clock follows the video's audio when it has any and wall time otherwise, so a call may advance zero, one or several frames; a video that reaches its end loops back to frame `0`. The five argument form stretches the frame into the rectangle from `(x, y)` to `(x2, y2)` inclusive, always with nearest-neighbor sampling and ignoring `videoScale`, `videoRotate` and `videoQuality`. The four argument form draws the frame at its native size transformed by `videoScale` and `videoRotate`, anchored at its top left corner or, when `centered` is `true`, at its center; the transformed frame is rebuilt only when the frame or the transform changes, and rotated corners are transparent. If the decoder has not produced a frame yet nothing is drawn.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle from `videoLoad`.
|
|
* `x`, `y` -- anchor in overlay coordinates.
|
|
* `x2`, `y2` -- opposite corner of the destination rectangle, inclusive.
|
|
* `centered` -- boolean. `true` places the center of the drawn frame at `(x, y)`, `false` its top left corner. Use `true` for rotated videos.
|
|
|
|
*Notes:* A video that is not drawn does not advance, so pausing is implicit while a clip is hidden. Draw from `onOverlayUpdate` only.
|
|
|
|
*Since:* 2.00. The scaled/rotated form with `centered` flag added in 2.10.
|
|
*See also:* <<videoload,videoLoad>>, <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Attract clip in a fixed box, looping over its own start and 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)
|
|
----
|
|
|
|
Returns the zero based index of the audio track a loaded video is playing. `videoLoad` selects the track named by `--audio` or the `AUDIO_TRACK` entry in `games.dat` (default `0`) when the file has that many tracks; a video with no audio at all reports `-1`.
|
|
|
|
*Returns:* integer track index.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Show which dub the cutscene is using in the options overlay.
|
|
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)
|
|
----
|
|
|
|
Returns how many audio streams a loaded video contains. A silent clip reports `0`; guard loops and modulo arithmetic against that.
|
|
|
|
*Returns:* integer count, `0` or more.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguage,videoGetLanguage>>, <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotrack,videoGetAudioTrack>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Only offer the language menu when the cutscene actually has choices.
|
|
cutscene = videoLoad("videos/briefing.mkv")
|
|
if videoGetAudioTracks(cutscene) > 1 then
|
|
buildLanguageMenu(cutscene)
|
|
else
|
|
languageMenuEnabled = false
|
|
end
|
|
----
|
|
|
|
[#videogetframe]
|
|
==== videoGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = videoGetFrame(id)
|
|
----
|
|
|
|
Returns the frame a loaded video is positioned on, zero based. The position only changes while the video plays and is drawn, and it wraps to `0` when the clip loops. Compare it with your own start and end markers to loop a section, or with `videoGetFrameCount` to detect the end of a one-shot clip.
|
|
|
|
*Returns:* integer frame number.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoseek,videoSeek>>, <<videogetframecount,videoGetFrameCount>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Return control to the player when the briefing has played through once.
|
|
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)
|
|
----
|
|
|
|
Returns the total number of frames in a loaded video, as counted from the file's frame table when it was indexed. Frames run from `0` to `count - 1`. Use it for progress bars, for wrapping a loop, or for detecting when a one-shot clip is almost done.
|
|
|
|
*Returns:* integer frame count.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetframe,videoGetFrame>>, <<videoseek,videoSeek>>, <<videoisplaying,videoIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Progress line under a loading clip: an outline frame and a fill that grows.
|
|
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)
|
|
----
|
|
|
|
Returns the height of a loaded video's frames in pixels, the native size of the file rather than the size it is drawn at. Combine it with `videoScale` to fit a clip into a box while keeping its aspect ratio.
|
|
|
|
*Returns:* integer height in pixels.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetwidth,videoGetWidth>>, <<videoscale,videoScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fit the clip into a 320x240 window without distorting it.
|
|
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)
|
|
----
|
|
|
|
Returns the language tag stored in the file for one audio track of a loaded video. The tag is a three letter ISO 639 code such as `"eng"` or `"jpn"`; a track without a three letter tag returns `"unk"`. A track outside `0` to `videoGetAudioTracks(id) - 1` terminates the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `track` -- zero based audio track index.
|
|
|
|
*Returns:* string language code.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videosetaudiotrack,videoSetAudioTrack>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Match the cutscene's dub to the language chosen for the disc.
|
|
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)
|
|
----
|
|
|
|
Looks up the English name of a language code. Unlike the other `video*` functions it takes no handle; it is a pure table lookup that accepts ISO 639-1 two letter codes and ISO 639-2 three letter codes (both the T and B forms), compared without regard to case. Unrecognized codes, including the `"unk"` returned for untagged tracks, give `"Unknown"`. `Framework.singe` aliases it as `discGetLanguageDescription` for symmetry with the disc API.
|
|
|
|
*Parameters:*
|
|
|
|
* `code` -- language code such as `"en"`, `"eng"` or `"jpn"`.
|
|
|
|
*Returns:* string language name, or `"Unknown"`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguage,videoGetLanguage>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Build a language-select menu from a video's tracks.
|
|
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()
|
|
----
|
|
|
|
Returns the stereo volume of a loaded video as two percentages. A freshly loaded video is at the `--volume_nonvldp` level on both channels (default `100`). Note that two values come back, left first.
|
|
|
|
Called with no argument it is a Hypseus extension answering the laserdisc's own volume instead, as a single number on Hypseus's `0` to `64` scale rather than as a percentage. Its games double that for <<musicsetvolume,`musicSetVolume`>>, whose range is `0` to `128`. The level follows `--volume_vldp`.
|
|
|
|
*Returns:* two integers, left and right, each `0` to `100`; or with no argument one integer, `0` to `64`.
|
|
|
|
*Since:* 2.00, the no-argument disc form 3.00
|
|
*See also:* <<videosetvolume,videoSetVolume>>, <<videoload,videoLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Duck the cutscene while a voice line plays, then restore it.
|
|
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)
|
|
----
|
|
|
|
Returns the width of a loaded video's frames in pixels, the native size of the file rather than the size it is drawn at.
|
|
|
|
*Returns:* integer width in pixels.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetheight,videoGetHeight>>, <<videoscale,videoScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Center a clip drawn at native size at the top of the overlay.
|
|
local x = (overlayGetWidth() - videoGetWidth(clip)) / 2
|
|
function onOverlayUpdate()
|
|
videoDraw(clip, x, 16, false)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#videoisplaying]
|
|
==== videoIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = videoIsPlaying(id)
|
|
----
|
|
|
|
Reports whether a loaded video is in the playing state, that is, whether `videoPlay` has been called since the last `videoPause`. A playing video still only advances while it is drawn. Videos start paused after `videoLoad`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoplay,videoPlay>>, <<videopause,videoPause>>, <<videogetframe,videoGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- One key toggles the picture-in-picture clip.
|
|
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)
|
|
----
|
|
|
|
Opens a video file and returns its handle. Any container and codec the bundled FFmpeg can decode is accepted. The engine builds or reads a frame index in a data directory named for the video's location under Singe's data root, so the first load of a large file can take a moment; failing to create that directory terminates the script. The video starts paused on frame `0`, with its audio track set from `--audio` or the `AUDIO_TRACK` entry in `games.dat` (default `0`) when the file has that many tracks, and both channels at the `--volume_nonvldp` level. Frames are decoded to RGB so they can be drawn on the overlay.
|
|
|
|
*Parameters:*
|
|
|
|
* `filename` -- path to the video file, relative to the game directory or inside a packed game.
|
|
|
|
*Returns:* integer video handle.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoplay,videoPlay>>, <<videodraw,videoDraw>>, <<videounload,videoUnload>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Load the attract clip for the selected game and cue its loop start.
|
|
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)
|
|
----
|
|
|
|
Pauses a loaded video. Its clock stops, its audio track pauses, and `videoDraw` keeps drawing the frame it stopped on. Pausing a video that is already paused is harmless.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoplay,videoPlay>>, <<videoisplaying,videoIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Freeze the attract clip while the calibration screen is up.
|
|
function calibrationBegin()
|
|
calibrating = true
|
|
if attractVideo then
|
|
videoPause(attractVideo)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#videoplay]
|
|
==== videoPlay
|
|
|
|
[source,text]
|
|
----
|
|
videoPlay(id)
|
|
----
|
|
|
|
Starts or resumes a loaded video from its current frame. The clock is restarted at that frame, so a video resumed after a long pause continues where it stopped rather than jumping ahead, and the audio track resumes with it. The video only advances while it is drawn with `videoDraw` or used as a material.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videopause,videoPause>>, <<videoseek,videoSeek>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Start the briefing over the paused disc.
|
|
function startBriefing()
|
|
discPause()
|
|
videoSeek(briefing, 0)
|
|
videoPlay(briefing)
|
|
briefingRunning = true
|
|
end
|
|
----
|
|
|
|
[#videoquality]
|
|
==== videoQuality
|
|
|
|
[source,text]
|
|
----
|
|
videoQuality(id, smooth)
|
|
----
|
|
|
|
Selects the filtering used when the four argument form of `videoDraw` scales or rotates the video. Any nonzero value selects `RENDER_SMOOTH` (bilinear), zero selects `RENDER_PIXELATED` (nearest-neighbor), the default. A change marks the transformed frame for rebuilding on the next draw. It has no effect on the stretched five argument form, which always uses nearest-neighbor, nor on an untransformed video.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `smooth` -- `RENDER_PIXELATED` (`0`) or `RENDER_SMOOTH` (`1`).
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A photographic clip shrunk into a corner looks better filtered.
|
|
pipVideo = videoLoad("videos/copilot.mkv")
|
|
videoScale(pipVideo, 0.4)
|
|
videoQuality(pipVideo, RENDER_SMOOTH)
|
|
videoPlay(pipVideo)
|
|
----
|
|
|
|
[#videorotate]
|
|
==== videoRotate
|
|
|
|
[source,text]
|
|
----
|
|
videoRotate(id, degrees)
|
|
----
|
|
|
|
Sets the rotation the four argument form of `videoDraw` applies, in degrees clockwise, using the same convention as `spriteRotate`. The angle is reduced modulo `360` and keeps its sign, so `450` and `90` are the same and `-90` rotates a quarter turn counterclockwise. The scale set by `videoScale` is kept. Rotation is applied about the frame's center, so draw rotated videos with `centered` set to `true`. A changed angle marks the transformed frame for rebuilding on the next draw.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `degrees` -- rotation in degrees, clockwise.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videorotateandscale,videoRotateAndScale>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Spin the radar feed slowly while it plays.
|
|
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)
|
|
----
|
|
|
|
Sets rotation and scale in one call, with the same rules as `videoRotate` and `videoScale`: degrees clockwise reduced modulo `360`, and scale factors that must be greater than `0`. Use it when both change every frame so the transformed frame is rebuilt once rather than twice.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `degrees` -- rotation in degrees, clockwise.
|
|
* `scale` -- uniform scale factor, `1.0` for native size.
|
|
* `scaleX`, `scaleY` -- separate horizontal and vertical factors.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videorotate,videoRotate>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A clip tumbles and shrinks away as it is dismissed.
|
|
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)
|
|
----
|
|
|
|
Sets the scale the four argument form of `videoDraw` applies, as a factor of the native size; `1.0` is unscaled. The rotation set by `videoRotate` is kept. A changed scale marks the transformed frame for rebuilding on the next draw. Factors of `0` or less cannot be rendered and abort the engine on the next `videoDraw`, so clamp animated scales above zero. The stretched five argument form of `videoDraw` ignores this setting.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `scale` -- uniform scale factor, greater than `0`.
|
|
* `scaleX`, `scaleY` -- separate horizontal and vertical factors, each greater than `0`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videorotateandscale,videoRotateAndScale>>, <<videoquality,videoQuality>>, <<videogetwidth,videoGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Show the cutscene at a quarter of its size in the corner while play continues.
|
|
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)
|
|
----
|
|
|
|
Positions a loaded video on `frame` and restarts its clock there. The play or pause state is unchanged: a playing video continues from the new frame at its next draw, a paused one shows it. The frame number wraps modulo the frame count, so `-1` is the last frame and a value past the end lands back inside the clip.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `frame` -- target frame, zero based.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetframe,videoGetFrame>>, <<videogetframecount,videoGetFrameCount>>, <<videoplay,videoPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Loop the attract clip between the two markers from games.dat.
|
|
function onOverlayUpdate()
|
|
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
|
|
if videoGetFrame(attractVideo) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
|
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#videosetaudiotrack]
|
|
==== videoSetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
videoSetAudioTrack(id, track)
|
|
----
|
|
|
|
Switches a loaded video to another of its audio tracks. Queued audio from the old track is dropped and playback realigns, so the change is heard almost immediately. Selecting the track already in use is a no-op. A track outside `0` to `videoGetAudioTracks(id) - 1` terminates the script, which for a silent clip means any value at all.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `track` -- zero based audio track index.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetaudiotrack,videoGetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The menu stores a per-game audio track for its attract clip.
|
|
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)
|
|
----
|
|
|
|
Sets the stereo volume of a loaded video as percentages. Values are clamped to `0` through `100` without complaint. The setting is remembered even for a silent clip and is reported back by `videoGetVolume`.
|
|
|
|
Called with one argument it is a Hypseus extension setting the laserdisc's own volume instead, on Hypseus's `0` to `64` scale rather than as a percentage: the twin of the bare <<videogetvolume,`videoGetVolume()`>>. A level outside `0` to `64` is ignored rather than fatal, which is what Hypseus does with one.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
* `left`, `right` -- channel volumes, `0` (silent) to `100` (full).
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetvolume,videoGetVolume>>, <<discaudio,discAudio>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fade the cutscene audio out over about a second as control returns.
|
|
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)
|
|
----
|
|
|
|
Stops the decoder and releases everything a loaded video holds: decoder, audio track, cached frames and the transformed surface. The handle is invalid afterward and passing it to any `video*` function terminates the script, so clear the variable that held it. Videos still loaded when the script ends are released by the engine.
|
|
|
|
*Parameters:*
|
|
|
|
* `id` -- video handle.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoload,videoLoad>>, <<videopause,videoPause>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Drop the selected game's assets before loading the next one.
|
|
function unloadGameAssets()
|
|
if attractVideo then
|
|
videoUnload(attractVideo)
|
|
attractVideo = nil
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vldp]
|
|
=== VLDP
|
|
|
|
The `vldp*` namespace predates Singe 2.00 and exists to keep older scripts running. `vldpGetHeight` and `vldpGetWidth` are the same bindings as `discGetHeight` and `discGetWidth`, `vldpSetVerbose` does nothing, and `vldpGetPixel` is the one member with no modern equivalent: it reads a pixel of the current disc frame at overlay coordinates and is still the standard way to do hit detection against pre-recorded video. Without a disc it returns black (see <<withoutadisc,Games Without a Disc>>).
|
|
|
|
WARNING: *Legacy.* Prefer <<discgetheight,discGetHeight>> and <<discgetwidth,discGetWidth>> in new code. `vldpGetPixel` remains current.
|
|
|
|
Hypseus Singe added eleven more members, kept under this prefix so its games run unchanged: `vldpFocusArea` and `vldpResetFocus` crop the disc picture, `vldpGetScale` and `vldpSetScale` report and change the display scale factor, `vldpGetRotate` and `vldpSetRotate` do the same for the presentation rotation, `vldpGetYUVPixel` reads raw samples, and `vldpFlash`, `vldpSetBlend`, `vldpSetLuma` and `vldpSetMonochrome` change how the picture itself looks. Each is marked "Hypseus extension" below. The scale and the rotation are two of the inputs to <<videorect,The Video Rectangle>>.
|
|
|
|
The four picture controls work on the decoded frame, on its luma and chroma planes, before it reaches the screen: `vldpSetBlend` and `vldpSetLuma` are applied one after the other as each frame is uploaded, `vldpSetMonochrome` replaces the chroma, and `vldpFlash` replaces the picture outright for one frame. They cost a pass over the luma plane per frame while they are on and nothing at all while they are off, they leave `vldpGetPixel` and `vldpGetYUVPixel` reading the picture as it was decoded, and with a frame file they follow the playback from segment to segment. All four go back to off when the script reloads (except monochrome, which goes back to whatever `--monochrome` asked for) and none of them touches a video drawn with `videoDraw`.
|
|
|
|
[#vldpflash]
|
|
==== vldpFlash
|
|
|
|
[source,text]
|
|
----
|
|
vldpFlash()
|
|
----
|
|
|
|
Hypseus extension. Puts one white picture on the disc, the way a real laserdisc player flashes when it is searched. It goes up at once and the next disc update takes it off again, so it lasts one drawn frame whether the disc is playing, paused or stopped.
|
|
|
|
The white is the value Hypseus flashes, 90 per cent luma with neutral chroma, not pure white, which is what a video signal's white actually is. The overlay, the GUIs and the bezel artwork are drawn over it as usual, so a heads-up display stays readable through the flash.
|
|
|
|
Without a disc, or on a disc whose first frame has not been decoded yet, the call does nothing.
|
|
|
|
*Parameters:* none.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<discsearch,discSearch>>, <<vldpsetluma,vldpSetLuma>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Punctuate a hit.
|
|
vldpFlash()
|
|
----
|
|
|
|
[#vldpfocusarea]
|
|
==== vldpFocusArea
|
|
|
|
[source,text]
|
|
----
|
|
vldpFocusArea(x, y, width, height)
|
|
vldpFocusArea()
|
|
----
|
|
|
|
Hypseus extension. Shows only a rectangle of the disc frame, given in disc pixels, stretched into the space the whole frame normally fills, with the renderer's usual smooth scaling; the aspect follows whatever `width` and `height` give. The rectangle is held inside the frame, and one that lies entirely outside it aborts the script. With no arguments the whole frame is shown again (Hypseus ignores that call and uses `vldpResetFocus`; Singe accepts both). The focus is a display effect only: `vldpGetPixel`, `vldpGetYUVPixel`, the mouse mapping and the disc on a 3D material all keep unfocused coordinates, so hit detection on a zoomed picture must be scaled by the caller. Without a disc the call does nothing. Reset when the script reloads.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- top-left corner in disc pixels.
|
|
* `width`, `height` -- size of the rectangle in disc pixels.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpresetfocus,vldpResetFocus>>, <<vldpgetwidth,vldpGetWidth>>, <<vldpgetpixel,vldpGetPixel>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Zoom into the middle 80% of the picture during the chase.
|
|
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()
|
|
----
|
|
|
|
Returns the height of the disc's video in pixels, or the canvas height without a disc. This is the same binding as `discGetHeight` registered under its pre-2.00 name.
|
|
|
|
*Returns:* integer height in pixels.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Legacy alias.* Use <<discgetheight,discGetHeight>> in new code.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetheight,discGetHeight>>, <<vldpgetwidth,vldpGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A 1.x script sizing its overlay from the disc.
|
|
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
|
|
centerX = vldpGetWidth() / 2
|
|
centerY = vldpGetHeight() / 2
|
|
----
|
|
|
|
[#vldpgetpixel]
|
|
==== vldpGetPixel
|
|
|
|
[source,text]
|
|
----
|
|
r, g, b = vldpGetPixel(x, y)
|
|
----
|
|
|
|
Reads the color of one pixel of the disc frame currently on screen. The coordinates are overlay coordinates: the engine divides them by the overlay-to-video scale and truncates, so you pass the same numbers you draw with. Returns `0, 0, 0` when there is no disc, when no frame has been decoded yet, or when the scaled coordinates fall outside the video. Games that use it typically tag targets in the source footage with a distinctive color and test the pixel under the crosshair on a trigger pull instead of authoring hit boxes for every frame.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- overlay coordinates; fractional values are accepted.
|
|
|
|
*Returns:* three integers, red, green and blue, each `0` to `255`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetframe,discGetFrame>>, <<mousegetposition,mouseGetPosition>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Lightgun hit check: targets are painted pure magenta on enemy frames.
|
|
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 extension. Returns the presentation rotation in degrees clockwise: the `--rotate` option, or whatever `vldpSetRotate` last set, and `0` when neither has. Only `0`, `90`, `180` and `270` are ever returned, because those are the only angles Singe turns to.
|
|
|
|
*Returns:* integer, `0`, `90`, `180` or `270`.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpsetrotate,vldpSetRotate>>, <<vldpgetscale,vldpGetScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A portrait cabinet wants its score panel down the long side.
|
|
if vldpGetRotate() == 90 or vldpGetRotate() == 270 then
|
|
layoutPanel(PANEL_TALL)
|
|
else
|
|
layoutPanel(PANEL_WIDE)
|
|
end
|
|
----
|
|
|
|
[#vldpgetscale]
|
|
==== vldpGetScale
|
|
|
|
[source,text]
|
|
----
|
|
scale = vldpGetScale()
|
|
----
|
|
|
|
Hypseus extension. Returns the display scale factor as a percentage: the `--scalefactor` option, or whatever `vldpSetScale` last applied, and `100` when neither has been given. Hypseus games divide `100` by it to size their overlay geometry to the shrunken picture. It has nothing to do with `vldpFocusArea` or the overlay resolution.
|
|
|
|
*Returns:* integer, `25` to `100`.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpsetscale,vldpSetScale>>, <<vldpfocusarea,vldpFocusArea>>, <<overlaygetwidth,overlayGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hypseus game code: the gun sights scale with the picture.
|
|
if vldpGetScale ~= nil then
|
|
gunScale = 100 / vldpGetScale()
|
|
else
|
|
gunScale = 1
|
|
end
|
|
----
|
|
|
|
[#vldpgetshift]
|
|
==== vldpGetShift
|
|
|
|
[source,text]
|
|
----
|
|
x, y = vldpGetShift()
|
|
----
|
|
|
|
The picture's offset within the window as percentages, the `--shiftx` and
|
|
`--shifty` options or whatever <<vldpsetshift,vldpSetShift>> last applied, and
|
|
`0, 0` when neither has been given. At the extreme a shift fills exactly the
|
|
room `--scalefactor` left, so `100` with the scale at `100` moves nothing.
|
|
|
|
*Returns:* two integers, each `-100` to `100`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vldpsetshift,vldpSetShift>>, <<vldpgetscale,vldpGetScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Report where the operator has put the picture.
|
|
local x, y = vldpGetShift()
|
|
fontPrint(10, 10, string.format("shift %d, %d", x, y))
|
|
----
|
|
|
|
[#vldpgetwidth]
|
|
==== vldpGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = vldpGetWidth()
|
|
----
|
|
|
|
Returns the width of the disc's video in pixels, or the canvas width without a disc. This is the same binding as `discGetWidth` registered under its pre-2.00 name.
|
|
|
|
*Returns:* integer width in pixels.
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Legacy alias.* Use <<discgetwidth,discGetWidth>> in new code.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetwidth,discGetWidth>>, <<vldpgetheight,vldpGetHeight>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A 1.x script sizing its overlay from the disc.
|
|
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
|
|
centerX = vldpGetWidth() / 2
|
|
centerY = vldpGetHeight() / 2
|
|
----
|
|
|
|
[#vldpgetyuvpixel]
|
|
==== vldpGetYUVPixel
|
|
|
|
[source,text]
|
|
----
|
|
y, u, v = vldpGetYUVPixel(x, y)
|
|
----
|
|
|
|
Hypseus extension. Reads the raw samples of one pixel of the disc frame currently shown, before any colour conversion: the luma and the two chroma samples, each `0` to `255`, with chroma at the 4:2:0 resolution of the decoded picture (neutral is `128`). The coordinates are overlay coordinates, scaled and truncated exactly as `vldpGetPixel` does. Returns `-1, -1, -1` when there is no disc, when no frame has been decoded yet, or when the scaled coordinates fall outside the video. Unaffected by `vldpFocusArea` and `vldpSetMonochrome`. Cheaper than `vldpGetPixel` when only the brightness matters.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- overlay coordinates; fractional values are accepted.
|
|
|
|
*Returns:* three integers, `Y`, `U` and `V`, each `0` to `255`, or `-1, -1, -1`.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpgetpixel,vldpGetPixel>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- The road is dark; anything bright under the car is an obstacle.
|
|
function carHitsSomething()
|
|
local y = vldpGetYUVPixel(carX + carWidth / 2, carY + carHeight)
|
|
return y >= 120
|
|
end
|
|
----
|
|
|
|
[#vldpresetfocus]
|
|
==== vldpResetFocus
|
|
|
|
[source,text]
|
|
----
|
|
vldpResetFocus()
|
|
----
|
|
|
|
Hypseus extension. Shows the whole disc frame again after `vldpFocusArea`, exactly as `vldpFocusArea()` with no arguments does. Hypseus's own name for the reset, kept so its games run unchanged.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpfocusarea,vldpFocusArea>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onChaseEnded()
|
|
vldpResetFocus()
|
|
end
|
|
----
|
|
|
|
[#vldpsetblend]
|
|
==== vldpSetBlend
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetBlend(enabled)
|
|
----
|
|
|
|
Hypseus extension. Smooths the disc picture down its rows: each luma sample becomes the average of itself and the samples immediately above and below it. This is Hypseus's own three tap blend filter, and it exists to take the hard edges off an interlaced transfer without softening the picture sideways.
|
|
|
|
The chroma planes are left alone, so the smoothing changes the brightness detail and not the colour. Turning it off puts the picture back exactly as it was decoded.
|
|
|
|
Without a disc the call does nothing but is still remembered, so it takes effect if a disc is loaded later. A non-boolean argument terminates the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpsetluma,vldpSetLuma>>, <<vldpsetmonochrome,vldpSetMonochrome>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A service screen switch for a noisy transfer.
|
|
vldpSetBlend(SETTINGS.softenVideo)
|
|
----
|
|
|
|
[#vldpsetluma]
|
|
==== vldpSetLuma
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetLuma(enabled [, level])
|
|
----
|
|
|
|
Hypseus extension. Brightens or darkens the disc picture. `level` runs from 0 to 8 and 4, the default, changes nothing: the distance from 4 is added to every luma sample in eighths, so 0 halves the brightness, 8 raises it by half, and the steps between are even. A level outside 0 to 8 terminates the script.
|
|
|
|
Since only the luma is changed, the colour stays where it is: raising the level brightens the picture without washing the colour out, which is what a real player's picture control does and what a colour multiplier would not. Turning it off, or asking for level 4, puts the picture back exactly as it was decoded.
|
|
|
|
`vldpSetLuma(true)` with no level therefore does nothing visible, which is Hypseus's behaviour as well.
|
|
|
|
Without a disc the call does nothing but is still remembered, so it takes effect if a disc is loaded later.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
* `level` -- optional number; 0 to 8, 4 being neutral. Default 4.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpsetblend,vldpSetBlend>>, <<vldpsetmonochrome,vldpSetMonochrome>>, <<vldpflash,vldpFlash>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A night scene the player cannot see; let them lift it.
|
|
vldpSetLuma(true, 6)
|
|
----
|
|
|
|
[#vldpsetmonochrome]
|
|
==== vldpSetMonochrome
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetMonochrome(enabled)
|
|
----
|
|
|
|
Hypseus extension. Shows the disc in luma only: the chroma planes are replaced by neutral samples when each frame is uploaded, so a paused disc changes at once. With a frame file the setting follows the playback from segment to segment. `vldpGetPixel` and `vldpGetYUVPixel` keep returning the colour samples, and extra videos drawn with `videoDraw` are unaffected. Pair it with `overlaySetMonochrome` for a black and white mode. Without a disc the call does nothing; reset when the script reloads. A non-boolean argument aborts the script.
|
|
|
|
*Parameters:*
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<overlaysetmonochrome,overlaySetMonochrome>>, <<vldpgetpixel,vldpGetPixel>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A dip switch picks colour or black and white.
|
|
vldpSetMonochrome(not dipChroma)
|
|
overlaySetMonochrome(not dipChroma)
|
|
----
|
|
|
|
[#vldpsetrotate]
|
|
==== vldpSetRotate
|
|
|
|
[source,text]
|
|
----
|
|
applied = vldpSetRotate(degrees)
|
|
----
|
|
|
|
Hypseus extension. Turns the whole presentation clockwise: the disc, the overlay, the GUIs, the 3D scene and the particles all turn together, and mouse and light gun positions are turned back so a click still lands where it is drawn. A quarter turn swaps the shape of the presented area, so a portrait window is filled rather than letterboxed into a strip.
|
|
|
|
Hypseus accepts any angle from `0` to `359`; Singe takes only `0`, `90`, `180` and `270`, and answers `false` for anything else instead of ending the script. An arbitrary angle would leave the mouse mapping and the Sinden border without a sensible meaning, and no game in the Hypseus library asks for one.
|
|
|
|
The rotation costs one screen sized render target and one extra blit per frame while it is not zero, and nothing at all while it is. Reset to the `--rotate` value when the script reloads.
|
|
|
|
*Parameters:*
|
|
|
|
* `degrees` -- integer, `0`, `90`, `180` or `270`.
|
|
|
|
*Returns:* boolean, true when the rotation was applied.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpgetrotate,vldpGetRotate>>, <<vldpsetscale,vldpSetScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A vertical shooter section on a pivoting monitor.
|
|
if not vldpSetRotate(90) then
|
|
debugPrint("This build will not turn the screen.")
|
|
end
|
|
----
|
|
|
|
[#vldpsetscale]
|
|
==== vldpSetScale
|
|
|
|
[source,text]
|
|
----
|
|
applied = vldpSetScale(percent)
|
|
----
|
|
|
|
Hypseus extension. Shrinks the picture about its centre to `percent` of the window, the running equivalent of `--scalefactor`, and rebuilds the video rectangle so the disc, the overlay, the GUIs, the 3D scene, the particles, any Sinden border and the mouse mapping all follow it. Values outside `25` to `100` answer `false` and change nothing, rather than ending the script.
|
|
|
|
Changes are limited to one every 15 milliseconds, as Hypseus limits its own held-key zoom; a call inside that window answers `false`, so a script driving the scale from a held key should keep calling until it takes. Reset to the `--scalefactor` value when the script reloads.
|
|
|
|
*Parameters:*
|
|
|
|
* `percent` -- integer, `25` to `100`.
|
|
|
|
*Returns:* boolean, true when the scale was applied.
|
|
|
|
*Since:* 3.00, as a Hypseus Singe extension.
|
|
*See also:* <<vldpgetscale,vldpGetScale>>, <<vldpsetrotate,vldpSetRotate>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Zoom out while the player holds the adjust key.
|
|
function onInputPressed(input)
|
|
if input == SWITCH_BUTTON3 then
|
|
vldpSetScale(vldpGetScale() - 5)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vldpsetshift]
|
|
==== vldpSetShift
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetShift(x, y)
|
|
----
|
|
|
|
Moves the picture within the window, the running equivalent of `--shiftx` and
|
|
`--shifty`, and rebuilds the video rectangle so the disc, the overlay, the
|
|
GUIs, the 3D scene, the particles, any Sinden border and the mouse mapping all
|
|
follow it. The values are percentages of the room `--scalefactor` left, so
|
|
with the scale at `100` there is nowhere to move to. A value outside `-100` to
|
|
`100` ends the script, unlike <<vldpsetscale,vldpSetScale>>, which answers
|
|
`false`.
|
|
|
|
*Parameters:*
|
|
|
|
* `x` -- integer, `-100` to `100`.
|
|
* `y` -- integer, `-100` to `100`.
|
|
|
|
*Returns:* nothing.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<vldpgetshift,vldpGetShift>>, <<vldpsetscale,vldpSetScale>>, <<singesavegeometry,singeSaveGeometry>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Nudge the picture left while the operator holds the key.
|
|
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(...)
|
|
----
|
|
|
|
Does nothing. It once toggled logging in the original video player; the call accepts any arguments, logs a trace line and returns. Engine and script tracing are now command line options (`--program` and `--trace`).
|
|
|
|
*Notes:*
|
|
|
|
WARNING: *Unimplemented.* Retained for backward compatibility only. Use `--trace` on the command line to log API calls.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<debugprint,debugPrint>>, <<discgetstate,discGetState>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Ported 1.x startup code. The call is ignored; run Singe with --trace instead.
|
|
vldpSetVerbose(true)
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
discSearch(TITLE_FRAME)
|
|
----
|
|
|
|
[#enginecallbacks]
|
|
=== Engine Callbacks
|
|
|
|
These are functions the script defines and Singe calls when the matching event happens; all are optional, and an undefined one is skipped. Input callbacks fire as events are read, `onOverlayUpdate` runs at most once every 15 milliseconds, and none of them runs while the pause key holds the game frozen (see <<pausing,Pausing>>). An error inside a callback ends the game with a traceback. In the threaded model `singeMain` takes the place of most per-frame logic (see <<threaded,Threaded>>): the other callbacks still fire, but `Framework.singe` installs its own `onOverlayUpdate` to drive the `singeMain` coroutine, so a threaded game defines `singeMain`, `onShutdown` and whichever input callbacks it wants.
|
|
|
|
[#oncontrollermoved]
|
|
==== onControllerMoved
|
|
|
|
[source,text]
|
|
----
|
|
function onControllerMoved(axis, value, which)
|
|
end
|
|
----
|
|
|
|
Called for every axis motion event from a connected controller, including motion inside the dead zone, so a stick under a thumb fires many times a second. Directions past `SINGE_DEAD_ZONE` also act as switches when `controls.cfg` maps them, so a game that only needs digital steering can map `GAMEPAD_0.AXIS_LEFT_X_L` and `GAMEPAD_0.AXIS_LEFT_X_R` and ignore this callback. The value is stored for `controllerGetAxis` before the callback runs.
|
|
|
|
*Parameters:*
|
|
|
|
* `axis` -- `0` through `5`, matching the `GAMEPAD_AXIS_*` constants.
|
|
* `value` -- raw position, `-32768` to `32767`; triggers rest at `0`.
|
|
* `which` -- controller index, `0` through `3`, the index `controllerGetAxis` and the `GAMEPAD_N` tables use.
|
|
|
|
*See also:* <<controllergetaxis,controllerGetAxis>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Analog steering from the first controller's left stick only.
|
|
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
|
|
----
|
|
|
|
Called when a logical input goes down or up. What arrives depends on the keyboard mode. In `MODE_NORMAL`, `what` is a `SWITCH_*` value: keys, controller buttons, axis directions past the dead zone, mouse buttons and the mouse wheel all arrive this way, translated through `controls.cfg`, each press once with no key repeat, and inputs mapped to no switch are ignored. `SWITCH_PAUSE` arrives only after `singeSetPauseKeyEnabled(false)`; otherwise the engine freezes the game on that key and the script never sees it. In `MODE_FULL`, `what` is the keysym of the key (the character value, not the scancode), repeats are delivered, and controller and mouse buttons pass `0`; every event also reaches `onKeyPressed` and `onKeyReleased`, which carry the scancode or button code. When the pause key freezes the game every switch the script holds is released through `onInputReleased` first, and on thawing whatever is still physically down is pressed again, so a game never sees a stale button. Presses within the first second after the script starts, or after the window regains focus, are treated as held over from before and swallowed together with their release. One switch arrives from no control at all: `SWITCH_MOUSE_DISCONNECT` is raised by the engine when a mouse or light gun is unplugged, once as a press and immediately once as a release, so a gun game can notice its gun going (see <<mousedevices,Mice, Guns, and Who Chooses>>).
|
|
|
|
*See also:* <<keyboardsetmode,keyboardSetMode>>, <<onkeypressedonkeyreleased,onKeyPressed>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Example
|
|
[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
|
|
----
|
|
|
|
Called in `MODE_FULL` only, for every key going down or up, with both the logical keysym (the character the key produces on the current layout) and the physical scancode (the key's position, compared against the `SCANCODE` table). Held keys repeat in this mode, which is what text entry wants. Controller and mouse buttons come through as well, with `keysym` `0` and their `GAMEPAD_N` or `MOUSE_N` code as `scancode`. For text use `keysym`; for key-as-button controls use `scancode` so the binding is the same on every layout. Keyboard mappings of the engine's own switches do not act in this mode, so a game in `MODE_FULL` must provide its own way out.
|
|
|
|
*See also:* <<keyboardsetmode,keyboardSetMode>>, <<keyboardgetmodifiers,keyboardGetModifiers>>, <<oninputpressedoninputreleased,onInputPressed>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- High score initials: text, so keysym; Return and Escape by position.
|
|
function onKeyPressed(keysym, scancode)
|
|
if scancode == SCANCODE.RETURN.value then
|
|
finishInitials()
|
|
elseif scancode == SCANCODE.BACKSPACE.value then
|
|
initials = initials:sub(1, -2)
|
|
elseif keysym >= string.byte("a") and keysym <= string.byte("z") and #initials < 3 then
|
|
initials = initials .. string.char(keysym):upper()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#onmousemoved]
|
|
==== onMidiMessage
|
|
|
|
[source,text]
|
|
----
|
|
function onMidiMessage(status, data1, data2, bytes)
|
|
end
|
|
----
|
|
|
|
Called for every message that arrives on the MIDI input port
|
|
<<midiopeninput,`midiOpenInput`>> opened, once per message, with the rest of the
|
|
frame's input. The three numbers are the message as a device sends it: `status`
|
|
carries the kind in its high four bits and the channel in its low four, counting
|
|
from `0`, so a note on channel 1 arrives as `0x90`. `bytes` is the whole message
|
|
as a string, which is what a system exclusive message needs and what a message
|
|
longer than three bytes has to be read from.
|
|
|
|
Messages that arrive while no port is open are not queued; nothing is delivered
|
|
from before the port was opened.
|
|
|
|
*Parameters:*
|
|
|
|
* `status` -- number; the status byte, `0x80` to `0xFF`.
|
|
* `data1` -- number; the first data byte, or `0` when the message has none.
|
|
* `data2` -- number; the second data byte, or `0` when the message has none.
|
|
* `bytes` -- string; the whole message, however long it is.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<midiopeninput,midiOpenInput>>, <<midisend,midiSend>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- A MIDI keyboard plays the game: middle C and up are the four fire buttons.
|
|
local MIDI_NOTE_ON = 0x90
|
|
|
|
function onMidiMessage(status, data1, data2, bytes)
|
|
-- The kind is the top four bits; the channel in the bottom four is ignored here.
|
|
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
|
|
|
|
[source,text]
|
|
----
|
|
function onMouseMoved(x, y, xRelative, yRelative, which)
|
|
end
|
|
----
|
|
|
|
Called for every mouse motion event, with the position and the motion since the previous event in overlay coordinates. In `MOUSE_SINGLE` mode the window's pointer position is converted to overlay coordinates and `which` is always `0`. In `MOUSE_MANY` mode each device's relative motion is integrated into its own position, clamped to the video area, and `which` is the device index; an absolute device such as a tablet reports its mapped position with `xRelative` and `yRelative` of `0`. The position is stored for `mouseGetPosition` before the callback runs, so a game may ignore this callback and poll instead. Drawing must still wait for `onOverlayUpdate`: keep the latest position in globals here and draw the cursor there.
|
|
|
|
*Parameters:*
|
|
|
|
* `x`, `y` -- position in overlay coordinates.
|
|
* `xRelative`, `yRelative` -- motion since the previous event, in overlay coordinates.
|
|
* `which` -- mouse index, `0` through `3`.
|
|
|
|
*See also:* <<mousegetposition,mouseGetPosition>>, <<mousesetmode,mouseSetMode>>, <<mousesetenabled,mouseSetEnabled>>
|
|
|
|
.Example
|
|
[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
|
|
----
|
|
|
|
Called once for each new contact between two bodies after a physics step, never again for a pair that stays in touch. A crate landing, a ball hitting the cabinet, the player's car clipping a wall. Contacts with triggers report through `onTrigger` instead. The events of a step are delivered together after it, before the frame is drawn.
|
|
|
|
*Parameters:*
|
|
|
|
* `nodeA`, `nodeB` -- the two bodies' nodes.
|
|
* `x`, `y`, `z` -- where they touched, in world units.
|
|
* `speed` -- how fast they met, in world units per second.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Hard landings make noise where they happen.
|
|
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
|
|
----
|
|
|
|
Called when a body enters or leaves a trigger made with `bodySetTrigger`, once on the way in and once on the way out, whatever the shapes involved (a mesh or compound shape touching in several places still counts as one body). Triggers never push anything, so this is the way to notice a body crossing a doorway, reaching a checkpoint or falling into a kill volume.
|
|
|
|
*Parameters:*
|
|
|
|
* `trigger` -- the trigger body's node.
|
|
* `other` -- the node that entered or left it.
|
|
* `entered` -- `true` on the way in, `false` on the way out.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<bodysettrigger,bodySetTrigger>>, <<oncollision,onCollision>>
|
|
|
|
.Example
|
|
[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
|
|
----
|
|
|
|
Called when a navigation agent reaches the target `navAgentMoveTo` gave it, once per arrival, after the navigation update of the frame. Send the agent somewhere else, or play its idle animation. A script that would rather poll can call `navAgentIsArrived` instead.
|
|
|
|
*Parameters:*
|
|
|
|
* `agent` -- the handle from `navAgentNew`.
|
|
|
|
*Since:* 3.00.
|
|
*See also:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Patrol between waypoints forever.
|
|
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
|
|
----
|
|
|
|
The one callback where drawing is allowed. Singe calls it at most once every 15 milliseconds while the game is not frozen; any `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint` or similar call made elsewhere will not appear on screen and may corrupt the overlay. Return `OVERLAY_UPDATED` (`1`) when the overlay changed so that Singe composites it over the video again, or `OVERLAY_NOT_UPDATED` (`0`) when nothing changed so the previous composite is reused; returning nothing counts as `OVERLAY_NOT_UPDATED`. The display is redrawn regardless whenever a new video frame arrives or a 3D scene is enabled, so the return value is an optimization for static screens. After it returns the engine clears the values behind `keyboardGetLastDown` and `keyboardGetLastUp`. In the threaded model `Framework.singe` defines this function itself to resume `singeMain` and always returns `OVERLAY_UPDATED`; do not define it as well.
|
|
|
|
*Returns:* `OVERLAY_UPDATED` or `OVERLAY_NOT_UPDATED`.
|
|
|
|
*See also:* <<singemain,singeMain>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<onshutdown,onShutdown>>
|
|
|
|
.Example
|
|
[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
|
|
----
|
|
|
|
Called once when the script ends, whether by `singeQuit`, the quit switch, the window closing, `scriptExecute` or `scriptPush`, after the last frame and before the Lua state is discarded. It is not called by `singeReload`. Save settings and free the sprites, sounds, fonts and videos the script loaded here; the engine stops every sound and frees whatever is left afterward, so a missed handle is not a leak, but explicit unloading keeps the shutdown order under the script's control. Do not call `singeQuit` from here.
|
|
|
|
*See also:* <<singequit,singeQuit>>, <<soundunload,soundUnload>>, <<scriptpush,scriptPush>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
saveHighScores()
|
|
spriteUnload(reticle)
|
|
fontUnload(hudFont)
|
|
soundUnload(shotClip)
|
|
soundUnload(coinClip)
|
|
end
|
|
----
|
|
|
|
[#onsoundcompleted]
|
|
==== onSoundCompleted
|
|
|
|
[source,text]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
end
|
|
----
|
|
|
|
Called with the channel number each time a channel stops: when its clip ends, when the last loop finishes, and when `soundStop` or `soundFullStop` halts it, so a script that chains sounds from here should know which channels it stopped itself. The mixer notes the stop on its own thread and the engine delivers the callback on the game thread between frames; while the pause key holds the game frozen the notices wait, and they are dropped when a script ends or reloads. Up to 64 completions are queued between frames; more than that in one frame are lost.
|
|
|
|
*Parameters:*
|
|
|
|
* `channel` -- the channel number the `soundPlay` call returned.
|
|
|
|
*See also:* <<soundplay,soundPlay>>, <<soundstop,soundStop>>, <<soundisplaying,soundIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Play the music playlist in order, forever.
|
|
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
|
|
----
|
|
|
|
The threaded model's entry point. When a script defines it, `Framework.singe` wraps it in a coroutine and installs an `onOverlayUpdate` that resumes the coroutine once per frame, so the function runs as a straight line of game logic that calls `singeYield()` (an alias of `coroutine.yield`) wherever it wants to let a frame pass. Everything drawn before a yield lands on that frame's overlay. Returning from `singeMain` calls `singeQuit`; an error inside it ends the game with a traceback. The input, sound, physics and navigation callbacks still fire between resumes, so a threaded game may mix them with polling.
|
|
|
|
*See also:* <<onoverlayupdate,onOverlayUpdate>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<singequit,singeQuit>>
|
|
|
|
.Example
|
|
[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
|
|
----
|
|
|
|
[#unimplementedandlegacy]
|
|
=== Hypseus Compatibility
|
|
|
|
IMPORTANT: Everything in this section exists so that a game written for Hypseus
|
|
Singe runs unchanged. *None of it is recommended for a new game.* Prefer the
|
|
Singe call each entry names: it is the one that will keep gaining features, and
|
|
it does not carry another engine's scales, argument orders or fixed answers.
|
|
|
|
Hypseus Singe registers a few names in its engine that Singe answers from
|
|
`Singe/Framework.singe` instead, so that a game written for Hypseus runs
|
|
unchanged. Each one is defined only when the engine does not already provide
|
|
it, so a future engine implementation wins. `keyboardCatchQuit` is documented
|
|
under <<keyboard,Keyboard>>; `setOverlayLinearScale` and `setOverlayOnTop` are
|
|
real engine calls under <<overlay,Overlay>>; `mainBezelLoaded` and the seven
|
|
`scoreBezel` calls are real engine calls under <<bezelref,Bezel>>; and
|
|
`getFValue`, `ratioGetX` and `ratioGetY` are real engine calls under
|
|
<<commandlineref,Command Line>>; `joyMouseEnable` is a real engine call
|
|
under <<joystick,Joystick>>; and the whole `music` family is now a set of real
|
|
engine calls under <<musicref,Music>>. The six `spriteAnim` names below are
|
|
plain aliases set after the legacy sprite wrapping, so they take whichever
|
|
argument order the rest of the sprite family is taking: Hypseus's own (handle
|
|
last) under `LEGACY_SPRITE_ARGS`, and Singe 3.00's (handle first) without it.
|
|
|
|
[cols="1,1",options="header"]
|
|
|===
|
|
| Function | Provided by Framework.singe as
|
|
| `setOverlaySize(preset [, width, height])` | `overlaySetResolution` with a chosen size: `1` is the disc's video size, `3` is `360x240`, `4` is the size given (ignored when it is missing, as in Hypseus) and anything else, `2` included, is half the disc on both axes. Without a disc `discGetWidth` and `discGetHeight` report the canvas, which is what a disc-less game draws into.
|
|
| `setOverlayResolution(width, height)` | An alias of <<overlaysetresolution,`overlaySetResolution`>>. Hypseus registers one function under both names.
|
|
| `rewriteStatus()` | Stub returning `false`. In Hypseus it reports whether asset paths are being rewritten for an `-espath` install; Singe's virtual file system finds a game's files where they were installed, so nothing is rewritten and the game keeps its own asset directory.
|
|
| `hypseusGetHeight()` | An alias of <<singegetheight,`singeGetHeight`>>.
|
|
| `hypseusGetWidth()` | An alias of <<singegetwidth,`singeGetWidth`>>.
|
|
| `takeScreenshot()` | An alias of <<singescreenshot,`singeScreenshot`>>. Hypseus refuses a second screenshot within five seconds; Singe has no such throttle.
|
|
| `mouseHowManyReal()` | An alias of <<mousehowmany,`mouseHowMany`>>. Hypseus counts real mice apart from the ones it derives from a gamepad; Singe has no derived mice, so its count is already the real one.
|
|
| `spriteAnimGetFrame(spr)` | An alias of <<spritegetframe,`spriteGetFrame`>>.
|
|
| `spriteAnimIsPlaying(spr)` | An alias of <<spriteisplaying,`spriteIsPlaying`>>.
|
|
| `spriteAnimLoop(loop, spr)` | An alias of <<spriteloop,`spriteLoop`>>.
|
|
| `spriteAnimPause(spr)` | An alias of <<spritepause,`spritePause`>>.
|
|
| `spriteAnimPlay(spr)` | An alias of <<spriteplay,`spritePlay`>>.
|
|
| `spriteAnimSetFrame(frame, spr)` | An alias of <<spritesetframe,`spriteSetFrame`>>.
|
|
|===
|
|
|
|
=== Unimplemented and Legacy
|
|
|
|
A handful of API functions are retained for script-level compatibility but do nothing. They are safe to call; they simply log a trace message and return. Prefer the listed replacements for new code.
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Function | Status | Replacement
|
|
| `discChangeSpeed` | No-op | -- (was for variable-speed playback on real LD hardware)
|
|
| `discSearchBlanking` | No-op | -- (was screen-blanking during seek on real LD hardware)
|
|
| `discSkipBlanking` | No-op | -- (same reason)
|
|
| `discSetFPS` | No-op | Framerate is read from the video file.
|
|
| `vldpGetHeight` | Works | <<discgetheight,`discGetHeight`>>
|
|
| `vldpGetWidth` | Works | <<discgetwidth,`discGetWidth`>>
|
|
| `vldpSetVerbose` | No-op | --
|
|
| `colorBackground(r,g,b)` | Works | Prefer the 4-argument form, `colorBackground(r, g, b, a)`.
|
|
| `colorForeground(r,g,b)` | Works | Prefer the 4-argument form, `colorForeground(r, g, b, a)`.
|
|
| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Aliases in `Framework.singe` | Prefer the `singe*` equivalents.
|
|
| `discPauseAtFrame` | Works (alias) | <<discsearch,`discSearch`>>.
|
|
|===
|
|
|
|
In addition, `Framework.singe` rebinds `random = { new = math.random }` so that pre-2.00 scripts using `random.new()` still work. New code should use `math.random` directly.
|