717 lines
25 KiB
Text
717 lines
25 KiB
Text
== Lesson 30: In a Cabinet
|
|
|
|
image::learn/30-cabinet.png[The finished lesson, 480]
|
|
|
|
There is a difference between a game and an arcade machine, and it is not the
|
|
wood. A machine switches itself on in the morning, plays to an empty room, takes
|
|
a coin from somebody who has never read a word about it, and is still running
|
|
at midnight with nobody having touched a keyboard, because there is no
|
|
keyboard. Everything in this lesson comes from that.
|
|
|
|
You do not need a cabinet to do any of it. A stick and two buttons on a desk,
|
|
or a gamepad, is enough to find every mistake in this lesson, and most of what
|
|
you learn here makes a game better on a desktop too.
|
|
|
|
=== There Is No Keyboard
|
|
|
|
Start with what is missing, because it is the thing that breaks games.
|
|
|
|
A cabinet has a stick, some buttons, a coin door, a start button, and somewhere
|
|
inside it a small switch an operator can reach. That is all. No Escape, no
|
|
letters, no mouse, no way to type. So:
|
|
|
|
* Every menu in your game has to be reachable with up, down, and a button. If
|
|
any screen in your game says "press Y to continue", it does not work in a
|
|
cabinet.
|
|
* Entering initials is three buttons and a letter that scrolls, not a text
|
|
field. That is why arcade games have three letter names.
|
|
* A prompt that names a key by its letter is wrong. Say "button 1", or draw the
|
|
button.
|
|
* There is no way out. A player cannot quit, and should not be able to.
|
|
|
|
Go through your game once, screen by screen, and ask of each: could somebody
|
|
get through this with a stick and one button? That pass on your own game is
|
|
worth more than the rest of this lesson.
|
|
|
|
=== controls.cfg
|
|
|
|
Since lesson three you have been reading input as `SWITCH_UP`,
|
|
`SWITCH_BUTTON1`, `SWITCH_COIN1`, and the rest, and you have never once said
|
|
which key those are. That is the point of them. Somewhere between the
|
|
microswitch in the cabinet and your `onInputPressed` is a file that decides,
|
|
and your game is not in that conversation.
|
|
|
|
The file is `controls.cfg`. It is Lua, like your script, and it is a list of
|
|
tables -- one per switch -- naming everything that can produce it:
|
|
|
|
[source,lua]
|
|
----
|
|
INPUT_1P_START = { SCANCODE.MAIN_1, GAMEPAD_0.BUTTON_START }
|
|
INPUT_1P_COIN = { SCANCODE.MAIN_5, GAMEPAD_0.BUTTON_LEFT_BUMPER }
|
|
INPUT_ACTION_1 = { SCANCODE.LCTRL, GAMEPAD_0.BUTTON_A }
|
|
INPUT_UP = { SCANCODE.UP, GAMEPAD_0.DPAD_UP, GAMEPAD_0.AXIS_LEFT_Y_U }
|
|
----
|
|
|
|
Each `INPUT_` name in the file becomes the matching `SWITCH_` value in your
|
|
game: `INPUT_ACTION_1` is the `SWITCH_BUTTON1` you have been testing for, and
|
|
`INPUT_1P_COIN` is `SWITCH_COIN1`. Every entry in a table produces the same
|
|
switch, so a game does not care whether the player pressed the key, the pad
|
|
button, or pushed the stick.
|
|
|
|
You do not have to write the whole file. Singe starts with its own defaults and
|
|
reads `controls.cfg` from four places in turn -- the folder Singe was started
|
|
in, the folder above the game's data directory, the game's data directory, and
|
|
the game's script folder -- and each one it finds overrides what came before,
|
|
key by key. So a cabinet has one file at the top for the whole machine, and a
|
|
game that needs one extra button has a four line file of its own. There is a
|
|
`controls.cfg.example` in the `Singe` folder to copy from.
|
|
|
|
`DEAD_ZONE` is in there too: how far a stick has to move before it counts as
|
|
pressed, in the raw units the hardware reports, where full deflection is
|
|
`32767`. The default is `15000`. A worn stick that creeps needs a bigger
|
|
number, and `DEAD_ZONES` sets one axis at a time for the one stick that is
|
|
tired. The manual's section on customizing the controls covers all of it.
|
|
|
|
=== Every Device Is a Gamepad
|
|
|
|
An arcade cabinet's buttons are wired to an encoder board, which the computer
|
|
sees as some kind of joystick, and it is nothing like an Xbox pad. Singe writes
|
|
a mapping for a device like that itself and then opens it as `GAMEPAD_0`, so it
|
|
reaches `controls.cfg` and your game exactly as a recognised pad does. There is
|
|
one family of device and no second vocabulary to learn.
|
|
|
|
The mapping it invents is a straight one: the board's buttons in order become
|
|
`BUTTON_A`, `BUTTON_B`, `BUTTON_X`, `BUTTON_Y` and so on. If your cabinet's
|
|
buttons are not wired in that order, they will come out in the wrong places,
|
|
and the fix is not in your game: `--program` traces the mapping Singe wrote in
|
|
a form you can edit and put in a `gamecontrollerdb.txt` beside the game.
|
|
|
|
When a button "does nothing", the menu's *Input Test* tool answers the question
|
|
in five seconds. It shows every device the engine can see, and the last switch
|
|
a game would have been given. Either the switch arrives and your game is
|
|
ignoring it, or it never arrives and the problem is the mapping or the wire.
|
|
|
|
=== Four Switches Are Not Yours
|
|
|
|
Most switches go straight to your game. Four do not: the engine acts on pause,
|
|
quit, screenshot, and the mouse grab toggle itself.
|
|
|
|
Pause is genuinely useful for free. While the engine owns it, the key mapped to
|
|
`INPUT_PAUSE` freezes everything -- the disc, every video, every sound, and
|
|
your script -- draws its own indicator, and thaws again on the next press.
|
|
Anything your game thought was held down is released properly and pressed again
|
|
afterwards, so a stick held through a pause never sticks.
|
|
|
|
Quit is the one to take away, which the next section does.
|
|
|
|
=== Coins, Credits, and Start
|
|
|
|
An arcade machine has a shape older than any of us, and players know it without
|
|
being told:
|
|
|
|
. *Attract.* Nobody is there. The machine shows what it is, blinks `INSERT
|
|
COIN`, and asks for nothing.
|
|
. *Credit.* A coin arrives as `SWITCH_COIN1`. Coins become credits at whatever
|
|
rate the operator set -- not always one for one.
|
|
. *Play.* `SWITCH_START1` spends a credit and starts.
|
|
. *Game over.* The score, briefly, and back to attract on its own. Nobody
|
|
presses anything to leave; the player has walked away.
|
|
|
|
Two details people get wrong. Coins are counted, not spent: at two coins per
|
|
credit, one coin has to sit there waiting for its partner, and a player who put
|
|
one in and walked off has left it for the next person. And the machine must
|
|
find its own way back to attract, because the last thing a player does is stop
|
|
playing.
|
|
|
|
=== Behind the Service Switch
|
|
|
|
Inside the coin door there is a button that the player never sees and the
|
|
operator uses constantly. It is `SWITCH_SERVICE`, and the engine hands it
|
|
straight to you.
|
|
|
|
An operator opening your service menu expects to find, roughly in this order:
|
|
|
|
* *the price* -- coins per credit, and a free play setting for a machine at
|
|
home or at a show;
|
|
* *the difficulty*, because a machine in a bar and a machine in an arcade want
|
|
different answers;
|
|
* *a way to clear the high score table*, which is the single most requested
|
|
thing in this list;
|
|
* *what the machine is* -- the engine version, the screen, where the data
|
|
lives -- so a fault report has facts in it;
|
|
* *a way out*, without turning anything off.
|
|
|
|
Everything an operator changes has to survive the power being cut, which in an
|
|
arcade means cut in the middle of a play, without warning, by somebody pulling
|
|
a plug. `saveSet` writes at the end of the frame, which is soon enough for a
|
|
score and not soon enough for a setting somebody just adjusted. `saveFlush()`
|
|
writes immediately, and a service menu is exactly what it is for.
|
|
|
|
The bundled menu's own service tools, behind the same switch, are worth
|
|
knowing since they are on every machine your game will run on: the input test,
|
|
the sound and light gun tests, the display geometry, the audio delay, the
|
|
online account, and a page that deletes any one game's saved data. Yours does
|
|
not have to duplicate them.
|
|
|
|
=== A Machine That Starts Itself and Never Quits
|
|
|
|
The cabinet is switched on at the wall. Nobody logs in. A game must be on the
|
|
screen in a minute or so and stay there.
|
|
|
|
Getting Singe to run at power on is your operating system's job rather than
|
|
Singe's -- the machine starts the `Menu.sh` or `Menu.bat` that the engine
|
|
writes beside your games, or your game directly. What is your job is what
|
|
happens for the rest of the day.
|
|
|
|
Take the quit key away:
|
|
|
|
[source,lua]
|
|
----
|
|
singeSetQuitKeyEnabled(false)
|
|
----
|
|
|
|
The engine then ignores the switch mapped to `INPUT_QUIT` and delivers it to
|
|
you as `SWITCH_QUIT` like any other, so you can ask first, save first, or do
|
|
nothing at all. The window's close button always quits, whatever you set, which
|
|
matters in a minute.
|
|
|
|
Then a handful of options belong to the machine rather than to your script, and
|
|
go in a `settings.cfg` beside the games -- the same names as the command line
|
|
options, with the dashes removed:
|
|
|
|
[source,lua]
|
|
----
|
|
fullscreen = true
|
|
screen = 2
|
|
idleexit = 900
|
|
startsilent = true
|
|
----
|
|
|
|
`idleexit` quits after that many seconds with no input of any kind, for a
|
|
cabinet that should hand the screen back to a menu or a front end.
|
|
`startsilent` starts muted until somebody touches something, which is how a
|
|
room full of machines is bearable. There are more; the manual's settings and
|
|
command line sections list every one.
|
|
|
|
=== Artwork Round the Picture
|
|
|
|
A real cabinet has a printed bezel around the screen: artwork with a hole in it
|
|
that the game shows through. Singe draws one for you.
|
|
|
|
----
|
|
Singe --bezel cabinet.png MyGame.game
|
|
----
|
|
|
|
The file is looked for in a `bezels` folder inside the game -- a packed game
|
|
can carry its own -- and then in a `bezels` folder in the data directory, so a
|
|
player can put their own artwork on a game without touching it. Beside the
|
|
image, named after it, a small Lua file says where the picture goes, in the
|
|
artwork's own pixels:
|
|
|
|
[source,lua]
|
|
----
|
|
-- bezels/cabinet.cfg
|
|
CUTOUT = { x = 200, y = 120, width = 800, height = 500 }
|
|
----
|
|
|
|
Because those numbers are the artwork's own, they stay right at any window
|
|
size: the image and the hole scale together. With no such file the picture
|
|
keeps the whole window as it always has.
|
|
|
|
By default the artwork is behind everything and your game covers the hole.
|
|
`--bezelflip` puts it in front instead, so only its transparency lets the game
|
|
through, which is what you want for soft or shaped edges -- and then your
|
|
overlay is underneath it too, unless you say otherwise:
|
|
|
|
[source,lua]
|
|
----
|
|
if mainBezelLoaded() then
|
|
setOverlayOnTop(true)
|
|
end
|
|
----
|
|
|
|
`mainBezelLoaded()` also tells you whether there is artwork at all, which is
|
|
how a game decides to draw its own frame when there is none.
|
|
|
|
=== A Monitor on Its Side
|
|
|
|
Plenty of arcade monitors are mounted vertically. Turn the whole presentation:
|
|
|
|
----
|
|
Singe --rotate=90 MyGame.game
|
|
----
|
|
|
|
The picture, the overlay, any GUI, a 3D scene, and the particles all turn
|
|
together, and mouse and light gun positions are turned back, so a click still
|
|
lands where the player sees it. A quarter turn also swaps the shape of the
|
|
area, so a tall window is filled rather than letterboxed into a strip.
|
|
|
|
Only `0`, `90`, `180`, and `270` are accepted. A script can change it while
|
|
running with `vldpSetRotate`, which answers `false` for anything else rather
|
|
than ending your game, and `vldpGetRotate()` reads it back -- which is worth
|
|
putting on a service screen, since "the picture is sideways" and "the monitor
|
|
is sideways" are different faults.
|
|
|
|
What rotation does not do is rewrite your game. A game drawn for a wide screen,
|
|
turned ninety degrees, is a wide game on its side. Designing for a tall screen
|
|
means designing for a tall screen.
|
|
|
|
=== The Script
|
|
|
|
The whole shape: attract, coin, credit, play, game over, service, and a quit
|
|
that asks. The game in the middle is one button and a fifteen second timer,
|
|
because the game is not the point this time -- the machine around it is.
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local ROUND_MS = 15000
|
|
local MENU = { "Coins per credit", "Difficulty", "Free play", "Clear high score", "Exit service" }
|
|
local HARDNESS = { "Easy", "Normal", "Hard" }
|
|
|
|
local coinsPerCredit = saveGet("coinsPerCredit", 1)
|
|
local difficulty = saveGet("difficulty", 2)
|
|
local freePlay = saveGet("freePlay", false)
|
|
local best = saveGet("best", 0)
|
|
|
|
local state = "attract"
|
|
local goBackTo = "attract"
|
|
local coins = 0
|
|
local credits = 0
|
|
local score = 0
|
|
local endsAt = 0
|
|
local item = 1
|
|
local saying = ""
|
|
local hasBezel = false
|
|
|
|
|
|
local function wrap(value, low, high)
|
|
if value < low then
|
|
return high
|
|
end
|
|
if value > high then
|
|
return low
|
|
end
|
|
return value
|
|
end
|
|
|
|
|
|
local function canStart()
|
|
return freePlay or credits > 0
|
|
end
|
|
|
|
|
|
local function insertCoin()
|
|
coins = coins + 1
|
|
while coins >= coinsPerCredit do
|
|
coins = coins - coinsPerCredit
|
|
credits = credits + 1
|
|
end
|
|
end
|
|
|
|
|
|
local function startGame()
|
|
if not freePlay then
|
|
credits = credits - 1
|
|
end
|
|
score = 0
|
|
endsAt = singeGetTicks() + ROUND_MS
|
|
state = "play"
|
|
end
|
|
|
|
|
|
local function endGame()
|
|
if score > best then
|
|
best = score
|
|
saveSet("best", best)
|
|
end
|
|
saveFlush()
|
|
endsAt = singeGetTicks() + 5000
|
|
state = "over"
|
|
end
|
|
|
|
|
|
local function settingOf(which)
|
|
if which == 1 then
|
|
return tostring(coinsPerCredit)
|
|
elseif which == 2 then
|
|
return HARDNESS[difficulty]
|
|
elseif which == 3 then
|
|
if freePlay then
|
|
return "on"
|
|
end
|
|
return "off"
|
|
elseif which == 4 then
|
|
return tostring(best)
|
|
end
|
|
return ""
|
|
end
|
|
|
|
|
|
local function adjust(step)
|
|
if item == 1 then
|
|
coinsPerCredit = wrap(coinsPerCredit + step, 1, 4)
|
|
saveSet("coinsPerCredit", coinsPerCredit)
|
|
elseif item == 2 then
|
|
difficulty = wrap(difficulty + step, 1, #HARDNESS)
|
|
saveSet("difficulty", difficulty)
|
|
elseif item == 3 then
|
|
freePlay = not freePlay
|
|
saveSet("freePlay", freePlay)
|
|
end
|
|
saveFlush()
|
|
end
|
|
|
|
|
|
local function serviceInput(what)
|
|
if what == SWITCH_UP then
|
|
item = wrap(item - 1, 1, #MENU)
|
|
elseif what == SWITCH_DOWN then
|
|
item = wrap(item + 1, 1, #MENU)
|
|
elseif what == SWITCH_LEFT then
|
|
adjust(-1)
|
|
elseif what == SWITCH_RIGHT then
|
|
adjust(1)
|
|
elseif what == SWITCH_BUTTON1 then
|
|
if item == 4 then
|
|
best = 0
|
|
saveSet("best", 0)
|
|
saveFlush()
|
|
saying = "High score cleared."
|
|
elseif item == 5 then
|
|
state = goBackTo
|
|
end
|
|
elseif what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then
|
|
state = goBackTo
|
|
end
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_QUIT then
|
|
if state == "confirm" then
|
|
singeQuit()
|
|
else
|
|
goBackTo = state
|
|
state = "confirm"
|
|
end
|
|
return
|
|
end
|
|
|
|
if state == "confirm" then
|
|
if what == SWITCH_BUTTON2 then
|
|
state = goBackTo
|
|
end
|
|
return
|
|
end
|
|
|
|
if state == "service" then
|
|
serviceInput(what)
|
|
return
|
|
end
|
|
|
|
if what == SWITCH_SERVICE then
|
|
goBackTo = state
|
|
saying = ""
|
|
state = "service"
|
|
return
|
|
end
|
|
|
|
if what == SWITCH_COIN1 then
|
|
insertCoin()
|
|
return
|
|
end
|
|
|
|
if state == "play" then
|
|
if what == SWITCH_BUTTON1 then
|
|
score = score + 10 * difficulty
|
|
end
|
|
return
|
|
end
|
|
|
|
if what == SWITCH_START1 and canStart() then
|
|
startGame()
|
|
end
|
|
end
|
|
|
|
|
|
local function drawAttract()
|
|
overlayPrint(2, 2, "ROCK DODGER")
|
|
if canStart() then
|
|
overlayPrint(2, 4, "PRESS START")
|
|
elseif (singeGetTicks() // 400) % 2 == 0 then
|
|
overlayPrint(2, 4, "INSERT COIN")
|
|
end
|
|
overlayPrint(2, 6, "High score " .. best)
|
|
end
|
|
|
|
|
|
local function drawService()
|
|
overlayPrint(2, 0, "SERVICE MENU")
|
|
for row, name in ipairs(MENU) do
|
|
if row == item then
|
|
overlayPrint(1, row + 1, ">")
|
|
end
|
|
overlayPrint(3, row + 1, name)
|
|
overlayPrint(24, row + 1, settingOf(row))
|
|
end
|
|
overlayPrint(2, 8, saying)
|
|
overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate())
|
|
if hasBezel then
|
|
overlayPrint(2, 11, "Bezel artwork loaded.")
|
|
else
|
|
overlayPrint(2, 11, "No bezel artwork.")
|
|
end
|
|
overlayPrint(2, 12, singeGetDataPath())
|
|
overlayPrint(2, 16, "Up/Down choose. Left/Right change. Button 1 does it.")
|
|
overlayPrint(2, 17, "Button 2 or the service switch leaves.")
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
|
|
if state == "play" and singeGetTicks() >= endsAt then
|
|
endGame()
|
|
elseif state == "over" and singeGetTicks() >= endsAt then
|
|
state = "attract"
|
|
end
|
|
|
|
if state == "service" then
|
|
drawService()
|
|
elseif state == "confirm" then
|
|
overlayPrint(2, 2, "QUIT THIS GAME?")
|
|
overlayPrint(2, 4, "Quit again to quit. Button 2 stays.")
|
|
elseif state == "play" then
|
|
overlayPrint(2, 2, "SCORE " .. score)
|
|
overlayPrint(2, 3, "TIME " .. ((endsAt - singeGetTicks()) // 1000))
|
|
overlayPrint(2, 5, "Button 1 scores. " .. HARDNESS[difficulty] .. ".")
|
|
elseif state == "over" then
|
|
overlayPrint(2, 2, "GAME OVER")
|
|
overlayPrint(2, 4, "You scored " .. score .. ".")
|
|
overlayPrint(2, 6, "High score " .. best)
|
|
else
|
|
drawAttract()
|
|
end
|
|
|
|
if state ~= "service" then
|
|
overlayPrint(2, 17, "CREDITS " .. credits .. " COINS " .. coins .. "/" .. coinsPerCredit)
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
singeSetQuitKeyEnabled(false)
|
|
hasBezel = mainBezelLoaded()
|
|
if hasBezel then
|
|
setOverlayOnTop(true)
|
|
end
|
|
----
|
|
|
|
Run it and press `5` for a coin, `1` for start, and `9` for the service
|
|
switch -- those are the defaults in the shipped `controls.cfg`. Then press
|
|
Escape, and read what it says before you press it again.
|
|
|
|
=== What Just Happened
|
|
|
|
[source,lua]
|
|
----
|
|
local coinsPerCredit = saveGet("coinsPerCredit", 1)
|
|
local difficulty = saveGet("difficulty", 2)
|
|
local freePlay = saveGet("freePlay", false)
|
|
----
|
|
|
|
The operator's settings are read once, at the top, with a sensible value for a
|
|
machine that has never been set up. `saveGet` with a second argument hands back
|
|
that value when nothing was saved, which is how a first run is told from a
|
|
later one.
|
|
|
|
[source,lua]
|
|
----
|
|
local function insertCoin()
|
|
coins = coins + 1
|
|
while coins >= coinsPerCredit do
|
|
coins = coins - coinsPerCredit
|
|
credits = credits + 1
|
|
end
|
|
end
|
|
----
|
|
|
|
Coins in, credits out, at whatever rate the operator set. The `while` rather
|
|
than an `if` is not caution about impossible input -- it is the operator
|
|
raising the price from four coins to one while six coins are sitting in there.
|
|
|
|
[source,lua]
|
|
----
|
|
if what == SWITCH_QUIT then
|
|
if state == "confirm" then
|
|
singeQuit()
|
|
else
|
|
goBackTo = state
|
|
state = "confirm"
|
|
end
|
|
return
|
|
end
|
|
----
|
|
|
|
The quit switch reaches your game only because of the
|
|
`singeSetQuitKeyEnabled(false)` at the very bottom of the script. Pressing it
|
|
once asks; pressing it again, from the asking screen, calls `singeQuit()`
|
|
yourself. `goBackTo` remembers where the player was, so saying no puts them
|
|
back in their game rather than on the attract screen.
|
|
|
|
This block is first in the function and it `return`s, so nothing else can
|
|
swallow the switch. Every other state in the game is handled below it.
|
|
|
|
[source,lua]
|
|
----
|
|
if state == "service" then
|
|
serviceInput(what)
|
|
return
|
|
end
|
|
----
|
|
|
|
The same shape again. While the service menu is open, every switch belongs to
|
|
it -- the stick does not steer the game underneath, a coin does not land in
|
|
the middle of an adjustment. One `if`, early, with a `return`. Any screen that
|
|
sits on top of another one needs this, and a game that grows three of them and
|
|
does not have it produces bugs that are almost impossible to describe.
|
|
|
|
[source,lua]
|
|
----
|
|
saveSet("coinsPerCredit", coinsPerCredit)
|
|
...
|
|
saveFlush()
|
|
----
|
|
|
|
Set, then flush. Without the flush, the write happens at the end of the frame,
|
|
which is fast but not immediate. An operator adjusts a setting and pulls the
|
|
plug out ten seconds later, and this is the one place in a game where that
|
|
actually happens.
|
|
|
|
[source,lua]
|
|
----
|
|
elseif (singeGetTicks() // 400) % 2 == 0 then
|
|
overlayPrint(2, 4, "INSERT COIN")
|
|
end
|
|
----
|
|
|
|
The blink. `singeGetTicks()` is milliseconds since the engine started, `//`
|
|
divides and throws away the remainder, so the number changes every four
|
|
hundred milliseconds, and `% 2` makes it alternate between zero and one. No
|
|
variable, no timer, nothing to reset: the clock was already counting.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate())
|
|
----
|
|
|
|
The operator's half of the service page. A version, a rotation, whether there
|
|
is artwork, and where the data lives. None of it is for the player, and all of
|
|
it is what you will wish you had when somebody telephones you about a machine
|
|
three hundred miles away.
|
|
|
|
[source,lua]
|
|
----
|
|
singeSetQuitKeyEnabled(false)
|
|
hasBezel = mainBezelLoaded()
|
|
----
|
|
|
|
At the bottom, outside every function, so it runs once when the script loads.
|
|
Neither is a thing to do every frame, and `mainBezelLoaded` cannot change while
|
|
the game runs.
|
|
|
|
=== Try It
|
|
|
|
. *Change the price.* Open the service menu, set coins per credit to two, and
|
|
put one coin in. Watch the credit not appear, and watch the coin counter on
|
|
the bottom line. Then quit, start it again, and see that the price stuck.
|
|
. *Play free.* Turn free play on and check that start works with no coins and
|
|
that credits do not go negative.
|
|
. *Add a page.* Put a second page of service information behind button 3 --
|
|
the fields of `singeGetSystemInfo()` are a good start, and the manual's entry
|
|
lists all of them.
|
|
. *Turn the screen.* Run it with `--rotate=90` and look at your layout. Then
|
|
make the service page readable at that rotation, which is harder than it
|
|
sounds and is exactly the work a vertical cabinet asks for.
|
|
. *Take the keyboard away.* Unplug it, or promise yourself not to touch it, and
|
|
play through every screen with a pad. Anything you cannot reach is a bug.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Delete the `SWITCH_QUIT` block from `onInputPressed`, leaving
|
|
`singeSetQuitKeyEnabled(false)` where it is, and run it.
|
|
|
|
Now nothing quits. Escape does nothing, `Q` does nothing, and your game has no
|
|
way out, which is precisely what you asked for. This is not a crash and there
|
|
is no message; the engine did exactly as it was told.
|
|
|
|
Close the window with its close button. That always works, whatever a script
|
|
says, and it is the door you left yourself. In full screen, `Alt-Enter` puts
|
|
you back into a window first.
|
|
|
|
It is worth feeling this one. Turning off the quit key is the only thing in
|
|
this book that can leave you with a program you cannot get out of, and the
|
|
habit that prevents it is simple: write the way out in the same sitting you
|
|
turn the quit key off, never later.
|
|
|
|
=== What You Learned
|
|
|
|
* A cabinet has no keyboard, so every screen in the game has to work with a
|
|
stick and a button.
|
|
* `controls.cfg` maps keys, pad buttons, and stick directions onto the
|
|
`SWITCH_` names your game already uses, and your game never learns which was
|
|
pressed.
|
|
* Four files of mappings are read in turn, each overriding the last, so a
|
|
machine-wide file and a per-game file can both exist.
|
|
* Every device Singe opens is a gamepad, including an arcade encoder board, and
|
|
the Input Test tool says what is arriving.
|
|
* The engine keeps pause, quit, screenshot, and mouse grab for itself; the
|
|
rest are yours.
|
|
* Coins are counted into credits at the operator's rate, and a machine returns
|
|
to attract by itself.
|
|
* `SWITCH_SERVICE` is the operator's switch, and what is behind it is the
|
|
price, the difficulty, a way to clear the scores, and what the machine is.
|
|
* `saveFlush()` writes a setting now, because a cabinet loses power without
|
|
warning.
|
|
* `singeSetQuitKeyEnabled(false)` hands the quit switch to your game. Write the
|
|
way out at the same moment.
|
|
* A bezel is artwork with a hole in it, and `--rotate` turns the whole
|
|
presentation for a vertical monitor.
|
|
|
|
=== The End
|
|
|
|
That is thirty lessons, and you can write a game.
|
|
|
|
Not a perfect one, and not a large one yet, but the gap between here and
|
|
either of those is time and practice rather than another book. You started
|
|
with five lines that put a word on a black screen. You now know what a
|
|
variable is, what a function is for, how to draw, how to make a noise, how to
|
|
tell when two things touched, how to keep a score somewhere it survives, how
|
|
to run film as a world, how to put a model in a room with a light on it, and
|
|
how to hand the whole thing to a stranger.
|
|
|
|
What you need now is the _Singe Manual_, not this book. It is the reference to
|
|
every function, every argument, and every return value, and it is the thing you
|
|
will actually have open while you work. It ships with the engine as
|
|
`Singe/Manual.pdf`. Read the entry before you use the call; there is a note in
|
|
most of them about the mistake people make with it.
|
|
|
|
Read the scripts that come with Singe, too. `Singe/Menu.singe`,
|
|
`Singe/Tools.singe`, and `Singe/Framework.singe` are not demonstrations written
|
|
to be read -- they are the menu and the service tools you have been using all
|
|
along, doing real work with the same functions you have. Working out how
|
|
somebody else's program does something is a skill of its own, and those are a
|
|
kind place to practise it.
|
|
|
|
And there is a second book, _Forge_, for describing a game rather than writing
|
|
one. It is a different road. You do not need it, but it is there.
|
|
|
|
The next game is yours. Make the small one first, finish it, and put it in
|
|
front of somebody. Finishing is the part nobody tells you is the hard part, and
|
|
it is the only part that turns a folder full of scripts into a game that
|
|
somebody played.
|
|
|
|
Go and build it.
|