flan/examples/core-input-gamepad.fln

245 lines
12 KiB
Plaintext

;;;; raylib [core] example - input gamepad
;;;;
;;;; examples/core/core_input_gamepad.c. Needed set-config-flags (and the
;;;; FLAG_ constants), both now bound.
;;;;
;;;; Three deliberate differences from the C, all of them things the port
;;;; could not or should not follow.
;;;;
;;;; **The pad artwork is gone, and with it the xbox/PS branches.** The C draws
;;;; `resources/ps3.png` or `resources/xbox.png` and picks between them by
;;;; matching `GetGamepadName` against four substrings. Neither image is in
;;;; this repository, and importing two binaries for one example is worse than
;;;; the alternative: the C has a third branch, for a pad it does not
;;;; recognise, which draws the whole thing out of rounded rectangles and
;;;; circles and needs no asset at all. That branch is what this ports, in
;;;; full. It is also the branch a Linux desktop usually takes anyway.
;;;;
;;;; **The name is drawn and not matched on.** get-gamepad-name copies
;;;; raylib's static text into the context allocator, which this program never
;;;; frees — so the name is read once per frame into the frame arena, and the
;;;; branches that matched on it are the artwork's and are gone with it.
;;;;
;;;; **The VIBRATE button is drawn and inert.** SetGamepadVibration is the one
;;;; call in the C this refuses to bind, and vendor/raylib/raylib.fln already
;;;; carries the paragraph saying why: its arity differs between raylib
;;;; versions with no 5.5 header here to settle it, so a guess is a corrupted
;;;; stack frame rather than a link error — and the symbol in libraylib.so.550
;;;; disassembles to a single TraceLog call and a jump. Binding it would be
;;;; binding a warning. The button is drawn because taking it out would hide
;;;; the finding; it is labelled so.
;;;;
;;;; With no pad attached this shows "GP0: NOT DETECTED" and nothing else,
;;;; which is the C's behaviour minus the greyed-out artwork.
import rl "vendor:raylib"
import d "digits.fln"
const screen-width = 800
const screen-height = 450
const stick-deadzone: f32 = 0.1
const trigger-deadzone: f32 = -0.9
once gamepad: i32
;; The C's deadzone test, which is a band around zero and not a clamp: inside
;; it the axis reads exactly 0, outside it the raw value passes through
;; unscaled. That is a step at the edge of the band and the C has it too.
fn deadzone(v: f32, limit: f32) -> f32
if v > 0.0 - limit and v < limit then 0.0 else v
;; The triggers rest at -1 rather than 0, which raylib does not normalise —
;; see the note on GamepadAxis in vendor/raylib/raylib.fln. So their deadzone
;; is a floor near the resting end and not a band around the middle.
fn trigger-deadzoned(v: f32) -> f32
if v < trigger-deadzone then -1.0 else v
;; The C's read-out loop is `for (i = 0; i < GetGamepadAxisCount(gamepad); i++)
;; DrawText(TextFormat("AXIS %i: %.02f", i, GetGamepadAxisMovement(gamepad, i)))`
;; — an axis selected by a loop variable, which is what this is: an integer
;; converts to an enum when the conversion is written by name.
;;
;; It used to be a six-armed cond over the members, because an enum is its own
;; type in the checker and a bare i32 does not fit one:
;;
;; expected rl/GamepadAxis, found i32
;;
;; That rule is unchanged and should be. It is what makes a misspelled
;; `:lft-x` an error *here*, at the call site, rather than a wrong axis read
;; later — which is the whole reason an enum is a type at all rather than a
;; pile of i32 constants. What changed is that the conversion can be said out
;; loud: the rule was never "an integer is dangerous", it was "an integer must
;; not arrive silently", and (rl/GamepadAxis index) is not silent.
;;
;; (The other escape stays closed, and should: a second declare-c of
;; GetGamepadAxisMovement with an i32 face is still refused — one declare-c
;; per C function — and a defn wrapper still cannot retype a parameter. It is
;; no longer needed.)
;;
;; An index raylib does not name converts too, rather than being refused, and
;; raylib bounds-checks its own axis and answers 0.0. So the (min 6 ...) clamp
;; at the call site is what keeps the read-out to the six axes raylib names —
;; load-bearing now rather than cosmetic, since it replaced the :else arm.
fn axis-at(pad: i32, index: i32) -> f32
rl/get-gamepad-axis-movement(pad, rl/GamepadAxis(index))
fn draw-pad-background() -> ()
rl/draw-rectangle-rounded(rl/Rectangle{.x 175.0 .y 110.0 .width 460.0 .height 220.0},
0.3, 16, rl/darkgray)
;; The three middle buttons and the four face buttons, as outlines. The
;; filled overlays go on top only while the button is down.
rl/draw-circle(365, 170, 12.0, rl/raywhite)
rl/draw-circle(405, 170, 12.0, rl/raywhite)
rl/draw-circle(445, 170, 12.0, rl/raywhite)
rl/draw-circle(516, 191, 17.0, rl/raywhite)
rl/draw-circle(551, 227, 17.0, rl/raywhite)
rl/draw-circle(587, 191, 17.0, rl/raywhite)
rl/draw-circle(551, 155, 17.0, rl/raywhite)
;; The d-pad cross: a light plate with a dark one inset, so a pressed
;; direction has something to show up against.
rl/draw-rectangle(245, 145, 28, 88, rl/raywhite)
rl/draw-rectangle(215, 174, 88, 29, rl/raywhite)
rl/draw-rectangle(247, 147, 24, 84, rl/black)
rl/draw-rectangle(217, 176, 84, 25, rl/black)
rl/draw-rectangle-rounded(rl/Rectangle{.x 215.0 .y 98.0 .width 100.0 .height 10.0},
0.5, 16, rl/darkgray)
rl/draw-rectangle-rounded(rl/Rectangle{.x 495.0 .y 98.0 .width 100.0 .height 10.0},
0.5, 16, rl/darkgray)
fn draw-pad-buttons() -> ()
if rl/is-gamepad-button-down(gamepad, :button-middle-left)
rl/draw-circle(365, 170, 10.0, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-middle)
rl/draw-circle(405, 170, 10.0, rl/green)
if rl/is-gamepad-button-down(gamepad, :button-middle-right)
rl/draw-circle(445, 170, 10.0, rl/blue)
if rl/is-gamepad-button-down(gamepad, :button-right-face-left)
rl/draw-circle(516, 191, 15.0, rl/gold)
if rl/is-gamepad-button-down(gamepad, :button-right-face-down)
rl/draw-circle(551, 227, 15.0, rl/blue)
if rl/is-gamepad-button-down(gamepad, :button-right-face-right)
rl/draw-circle(587, 191, 15.0, rl/green)
if rl/is-gamepad-button-down(gamepad, :button-right-face-up)
rl/draw-circle(551, 155, 15.0, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-left-face-up)
rl/draw-rectangle(247, 147, 24, 29, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-left-face-down)
rl/draw-rectangle(247, 201, 24, 30, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-left-face-left)
rl/draw-rectangle(217, 176, 30, 25, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-left-face-right)
rl/draw-rectangle(271, 176, 30, 25, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-left-trigger-1)
rl/draw-rectangle-rounded(rl/Rectangle{.x 215.0 .y 98.0 .width 100.0 .height 10.0},
0.5, 16, rl/red)
if rl/is-gamepad-button-down(gamepad, :button-right-trigger-1)
rl/draw-rectangle-rounded(rl/Rectangle{.x 495.0 .y 98.0 .width 100.0 .height 10.0},
0.5, 16, rl/red)
fn draw-stick(cx: i32, cy: i32, ax: f32, ay: f32, thumb-down: bool) -> ()
rl/draw-circle(cx, cy, 40.0, rl/black)
rl/draw-circle(cx, cy, 35.0, rl/lightgray)
rl/draw-circle(cx + i32(ax * 20.0), cy + i32(ay * 20.0), 25.0,
if thumb-down then rl/red else rl/black)
fn main() -> ()
;; Before init-window, and it has to be: raylib reads the flags while it is
;; creating the context, so the same call afterwards is accepted, logged, and
;; has no effect on the window that already exists.
rl/set-config-flags(rl/flag-msaa-4x-hint)
rl/init-window(screen-width, screen-height,
"raylib [core] example - input gamepad")
defer rl/close-window()
rl/set-target-fps(60)
until rl/window-should-close()
;; The frame arena holds the pad's name for one frame.
free-all(context/temp)
;; Update
if rl/is-key-pressed(:key-left) and gamepad > 0
gamepad -= 1
if rl/is-key-pressed(:key-right)
gamepad += 1
let axis-count = min(6, rl/get-gamepad-axis-count(gamepad))
let vibrate-rect = rl/Rectangle{.x 10.0, .y 90.0 + 20.0 * f32(axis-count), .width 75.0, .height 24.0}
;; Draw
rl/with-drawing:
rl/clear-background(rl/raywhite)
if rl/is-gamepad-available(gamepad)
;; The C draws `TextFormat("GP%d: %s", gamepad, GetGamepadName(...))`.
;; TextFormat is variadic and unbound, so the pieces are drawn in turn.
rl/draw-text("GP", 10, 10, 10, rl/black)
let x = 10 + rl/measure-text("GP", 10)
let x = x + d/draw-int(gamepad, x, 10, 10, rl/black)
rl/draw-text(": ", x, 10, 10, rl/black)
rl/draw-text(with-allocator(context/temp, rl/get-gamepad-name(gamepad)),
x + rl/measure-text(": ", 10), 10, 10, rl/black)
let lx = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-left-x),
stick-deadzone)
let ly = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-left-y),
stick-deadzone)
let rx = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-right-x),
stick-deadzone)
let ry = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-right-y),
stick-deadzone)
let lt = trigger-deadzoned(rl/get-gamepad-axis-movement(gamepad, :axis-left-trigger))
let rt = trigger-deadzoned(rl/get-gamepad-axis-movement(gamepad, :axis-right-trigger))
draw-pad-background()
draw-pad-buttons()
draw-stick(345, 260, lx, ly,
rl/is-gamepad-button-down(gamepad, :button-left-thumb))
draw-stick(465, 260, rx, ry,
rl/is-gamepad-button-down(gamepad, :button-right-thumb))
;; The triggers as bars filling upward from a grey track. The +1
;; and the halving turn raylib's [-1, 1] into [0, 1].
rl/draw-rectangle(151, 110, 15, 70, rl/gray)
rl/draw-rectangle(644, 110, 15, 70, rl/gray)
rl/draw-rectangle(151, 110, 15, i32((1.0 + lt) / 2.0 * 70.0), rl/red)
rl/draw-rectangle(644, 110, 15, i32((1.0 + rt) / 2.0 * 70.0), rl/red)
rl/draw-text("DETECTED AXIS [", 10, 50, 10, rl/maroon)
let x = 10 + rl/measure-text("DETECTED AXIS [", 10)
rl/draw-text("]:", x + d/draw-int(axis-count, x, 50, 10, rl/maroon), 50,
10, rl/maroon)
for i in range(axis-count)
rl/draw-text("AXIS ", 20, 70 + 20 * i, 10, rl/darkgray)
let x = 20 + rl/measure-text("AXIS ", 10)
y = 70 + 20 * i
x += d/draw-int(i, x, y, 10, rl/darkgray)
rl/draw-text(": ", x, y, 10, rl/darkgray)
x += rl/measure-text(": ", 10)
;; The raw reading, not the deadzoned one — the C shows the raw
;; value here too, which is what makes the deadzone visible as a
;; difference between this row and the stick.
d/draw-f32(axis-at(gamepad, i), 2, x, y, 10, rl/darkgray)
;; Drawn and inert. See the header: SetGamepadVibration is a stub in
;; this raylib and is not bound.
rl/draw-rectangle-rec(vibrate-rect, rl/skyblue)
rl/draw-text("VIBRATE", i32(vibrate-rect.x) + 14,
i32(vibrate-rect.y) + 1, 10, rl/darkgray)
rl/draw-text("(not bound: raylib stub)", 95, i32(vibrate-rect.y) + 7,
10, rl/gray)
;; -1 when nothing is pressed, which is why the binding answers an
;; i32 and not a GamepadButton.
;; draw-int answers the width it drew, so a branch that ends in one
;; has type i32 while its sibling has type () and the `if` will not
;; typecheck — "expected i32, found ()". Both arms end in a
;; draw-text here, which is the tidy way out; where that is awkward a
;; trailing `()` is the other.
let b = rl/get-gamepad-button-pressed()
if b >= 0
rl/draw-text("DETECTED BUTTON: ", 10, 430, 10, rl/red)
d/draw-int(b, 10 + rl/measure-text("DETECTED BUTTON: ", 10), 430, 10,
rl/red)
()
else
rl/draw-text("DETECTED BUTTON: NONE", 10, 430, 10, rl/gray)
else
rl/draw-text("GP", 10, 10, 10, rl/gray)
let x = 10 + rl/measure-text("GP", 10)
rl/draw-text(": NOT DETECTED",
x + d/draw-int(gamepad, x, 10, 10, rl/gray), 10, 10,
rl/gray)
rl/draw-text("left/right arrows select another pad", 10, 30, 10,
rl/lightgray)