== 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 reference'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 reference'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 Reference_, 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.