Skip to content

Tilt the world by tilting the device (gyro tilt controls) - #10

Draft
MHunterG wants to merge 9 commits into
night-60fpsfrom
feature/gyro-tilt
Draft

MHunterG wants to merge 9 commits into
night-60fpsfrom
feature/gyro-tilt

Conversation

@MHunterG

Copy link
Copy Markdown
Contributor

LocoRoco is played by tilting the world with L and R (both together jump). With Controls > Tilt controls > Tilt with motion on, tilting a gamepad with a gyroscope, a phone or a Steam Deck does the same. It is off by default. The framework part is TeamGDB/PortableKit#41; this PR points the submodule at purun/tilt-controls (e73a866 plus that commit) and opts the profile in.

How the game reads tilt (traced, not assumed)

  • The pad reader at 0x08A37DD4 calls sceCtrlReadBufferPositive(…, 1) and sceCtrlReadLatch every frame. It stores the held buttons (u32), the latch's presses and releases (u16 each) and the stick as floats in a struct at 0x08B2D738. Its play tests L (0x100) and R (0x200) as held bits, also as 0x300 for both.
  • Hold R for 60 frames: the world eases towards the right over about 15–30 frames, and eases back over about 35–75 frames after release.
  • R on half or a quarter of the frames leaned the world visibly less than holding it. So the tilt is eased, and a small device roll presses R part of the time (TiltControls::proportional).
  • The stick does not tilt the world (full left and right for 60 frames, checked in the [pad] trace and captures).
  • L+R: a 4-frame press gives a small hop at once; held and then released, a bigger jump. No flick-to-jump gesture was added, because it cannot be shown reliable without hardware. The jump stays on L and R together, and a shoulder button the player presses takes over from the tilt, so a tilt never becomes an accidental jump.

Settings (README, Tilt controls)

  • Gyro mode: Angle (default; input only, picture untouched), Angle + level horizon (experimental; the picture is turned against the device and zoomed just enough to cover the screen, up to 10° by default, and the port's UI stays upright), Rate (turning tilts, and the tilt stays and fades).
  • Full tilt 12°, dead zone 4° with 1.5° hysteresis, invert, level horizon limit.
  • Re-centre: in the menu, R3, at start, and when the menu closes.
  • Variables: PURUN_TILT, PURUN_TILT_MODE, PURUN_TILT_FULL, PURUN_TILT_DEAD_ZONE, PURUN_TRACE_TILT.
  • Steam Deck: Steam Input hides the gyroscope. Either let Steam map it onto L and R, or disable Steam Input for the shortcut so SDL's own Deck driver exposes it.

How it was verified

macOS 27 on an M1, fresh in-place install from the European ISO into a scratch data dir, with a copy of an after-level-1 save. Bounded windowed NO_AUDIO runs driven with PURUN_INPUT_LIVE and the new tilt and gyro script steps:

  • The sensor enumeration log.
  • Angle mode:
    • 3° presses nothing.
    • 7° pulses R and the world leans part way.
    • 15° holds R.
    • -15° holds L.
  • R3 re-centre.
  • Rate mode: quick turn, slow return, fade.
  • The menu section and saving to settings.ini.
  • Level horizon at 0°, 5°, 10° and 15° (capped): the direction is right and no corner is uncovered.
  • Unit tests portablekit_tilt_tests and portablekit_settings_tests.

Not verified

  • Any real hardware: DualShock 4, DualSense, Switch Pro, the Steam Deck (either Steam Input setting), phones. Also Linux, Windows and Android.
  • How the thresholds feel in the hand.

Merging with fix/confirm-cross

Only the portablekit pointer conflicts. purun/tilt-controls and purun/confirm-cross merge cleanly in PortableKit; point the submodule at their merge. The profile edits don't overlap.

🤖 Generated with Claude Code

MHunterG and others added 9 commits September 29, 2026 23:24
LocoRoco is played by tilting its world with L and R. With Controls >
Tilt controls on, tilting a gamepad with a gyroscope, a phone or a Steam
Deck does the same (off by default).

How the game reads tilt, traced rather than assumed (PURUN_TRACE_PAD,
the pad reader at 0x08A37DD4, and runs with scripted input):
- Every frame it reads one buffer sample and the latch, and keeps the
  held buttons, presses, releases and the stick; its play tests L
  (0x100) and R (0x200) as held bits, and L and R together jump.
- Holding a side eases the world towards it and releasing eases it
  back: pressing R on half or a quarter of the frames leaned the world
  visibly less than holding it, so a small roll of the device presses
  for part of the frames (TiltControls::proportional).
- The stick does not tilt the world.

- The profile names L and R as the tilt buttons (GameProfile::tilt).
- PortableKit moves to purun/tilt-controls: e73a866 with the tilt
  controls (TeamGDB/PortableKit, feature/tilt-controls for main).
- The README describes the settings, the three gyro modes, where the
  motion comes from on each platform, and the two ways to use a Steam
  Deck's gyroscope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortableKit now reads the Deck's motion sensors straight from its
controller beside Steam Input, switching them on while tilt controls are
on, so Tilt with motion works on a Deck started from Steam without turning
Steam Input off. The README says so, names PURUN_TILT_DECK=0, and drops
the advice to turn Steam Input off or map the gyroscope in Steam.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortableKit moves the Deck controller's reads and the setting that
switches its motion unit on off the main thread, so the device cannot
hold up a frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortableKit now turns the picture back against the game's own tilt of its
world, read from the projection LocoRoco 2 turns by up to 30 degrees,
rather than against the device. The README describes the mode that way,
says the level limit goes to 30 degrees, and names
PURUN_TILT_LEVEL_FROM=device for the old behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortableKit adds a Jump button for tilt controls (L3 by default, bindable
to a face button, and a keyboard Jump action) and keeps L and R together a
jump whatever the device's roll. Angle + level horizon is no longer offered
in the menu; PURUN_TILT_MODE=horizon, or input.tilt_mode=level, still turns
it on. The README and the menu's note about jumping say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortableKit's stall watchdog writes every emulated thread's state to the
log when no frame has come for five seconds outside the menu, to find why
the Steam Deck build stops. PURUN_STALL_WATCHDOG=0 turns it off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortableKit reads the window's events while the stall watchdog sees the
game stalled, so closing it or SIGTERM ends it instead of needing SIGKILL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The framework's keyboard is for a game with a camera: W A S D to move, the
mouse to look, L on Q. LocoRoco is played by tilting, so Purun now starts
with both hands on tilt (Left arrow or A, Right arrow or D), Space to jump,
Shift for ○, Enter for × (confirm), the D-pad on the arrows' Up and Down
and on W, S, Q and E, and the mouse off. The game's menus and map read the
D-pad, the stick and held L and R in the same handler, so the arrows move
there too. Saved bindings stay; only what settings.ini does not name takes
these (PortableKit's GameProfile::keyboard_defaults).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
As on a PSP, the four arrows are the D-pad; L moves to A or Q and R to D or
E, beside them on the left hand. W and S are no longer bound, and the rest
stays: Space jumps, Shift is ○, Enter or Z is ×, C and X are △ and □, Tab
or P is START, Backspace SELECT, and the mouse is off. Saved bindings stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant