mobile-web
Use when a Godot game targets phones, tablets or browsers: touch controls, notches and safe areas, screen sizes and orientation, battery and pausing, Android or iOS exports, web builds, hosting on itch.io or GitHub Pages, or testing in Chrome, Firefox and Edge.
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-13
- Stars
- ★ 1
- Forks
- 0
01 前言 Front Matter
- name
- mobile-web
- description
- Use when a Godot game targets phones, tablets or browsers: touch controls, notches and safe areas, screen sizes and orientation, battery and pausing, Android or iOS exports, web builds, hosting on itch.io or GitHub Pages, or testing in Chrome, Firefox and Edge.
03 SKILL.md 正文
Phones, tablets and browsers
Measured on Godot 4.7.2 (Android and web exports built and run here; no iOS, no Android device); sources and the
measurements: ${CLAUDE_PLUGIN_ROOT}/docs/mobile-web.md.
1. What C# can target in Godot 4.7
| Platform | C# | Notes |
|---|---|---|
| Web | no | the export stops: "Exporting to Web is currently not supported in Godot 4 when using C#/.NET". A web build needs a GDScript game (or a GDScript demo). doctor fails a C# project with a Web preset |
| Android | experimental | use a Gradle build (AAB for Google Play): 39 MB here on net8.0. The prebuilt template needs net9.0 and made a 102 MB APK full of static .a files. doctor fails the prebuilt route on net8.0 |
| iOS | experimental | export only on macOS with Xcode; Apple wants the iOS 26 SDK (Xcode 26) for uploads since April 2026 |
2. Every screen: layout
layout.json in the project turns on the layout runner. It lays out each scene, headless, on a portrait phone
(1080x2400), a landscape phone, a tablet and a 3440x1440 ultrawide, and fails on controls that are:
- off-screen;
- unsafe-area: under the notch, the status bar or the home indicator;
- small-target: under 48 dp on touch (Android 48 dp, Apple 44 pt), under 24 CSS px with a mouse (WCAG 2.2);
- small-control, with
"minControlDp": 75: on touch profiles, a game control under 75 dp. Game controls are everyTouchScreenButton(measured from its shape, or its texture) and every Control in the grouptouch_control.
{ "scenes": ["res://ui/menu.tscn", "res://ui/hud.tscn"], "minControlDp": 75,
"profiles": ["phone_portrait", "phone_landscape", "tablet", "ultrawide", "foldable:1812x2176@374:touch:insets=0,96,0,48"] }
pwsh ${CLAUDE_PLUGIN_ROOT}/ci/godot/hyperdot.ps1 test -Runners layout -Json -Quiet
A UI designed at 1280x720 and shown on a portrait phone shrinks: a 64 px button became 21 dp. Design phone UI in portrait (or choose the base size for the orientation the game ships in).
Safe areas. DisplayServer.GetDisplaySafeArea() works on Android and iOS only; it returns the whole window
elsewhere, in screen pixels. GetDisplayCutouts() is Android only, and browsers report no notch at all. Keep the UI in
a SafeArea autoload's rectangle. The runner calls its SetInsets with each profile's notch, so a game that uses one
passes, and one that ignores the notch is caught. --safe-area=0,128,0,66 after -- fakes a notch on desktop.
public partial class SafeArea : Node // autoload named SafeArea
{
[Signal] public delegate void ChangedEventHandler();
private Rect2I _override;
public void SetInsets(int left, int top, int right, int bottom) // window pixels; zeros remove the fake notch
{
Vector2I window = GetTree().Root.Size;
_override = left + top + right + bottom > 0 ? new Rect2I(left, top, window.X - left - right, window.Y - top - bottom) : new Rect2I();
EmitSignal(SignalName.Changed);
}
public Rect2I WindowRect()
{
var window = new Rect2I(Vector2I.Zero, GetTree().Root.Size);
if (_override.HasArea()) return _override;
if (!OS.HasFeature("mobile")) return window;
Rect2I safe = DisplayServer.GetDisplaySafeArea(); // screen pixels
safe.Position -= DisplayServer.WindowGetPosition();
return safe.Intersection(window);
}
// What Controls use: the safe rectangle after stretching.
public Rect2 CanvasRect() => GetTree().Root.GetFinalTransform().AffineInverse() * new Rect2(WindowRect().Position, WindowRect().Size);
}
A full-rect MarginContainer sets its four margin_* theme constants from CanvasRect() on Changed (and on
Root.SizeChanged). The GDScript version is in the kit's tests/layout-fixture; the C# SafeArea and SafeMargins are in docs/mobile-web.md.
Orientation. The default is landscape (display/window/handheld/orientation; an exported APK said
screenOrientation=0). A portrait game must set it, and also its viewport size: Godot does not swap them.
A game that supports both (sensor) re-lays out on Root.SizeChanged at any moment, never only in _Ready; in
landscape both thumbs rest low at the sides, so critical buttons go in reach there, with a left-handed mirror. Test by
rotating mid-fight, during loading and between a press and its release (a UI flow's hold, resize and release
steps, ui-flows), and check text at the smallest aspect ratio in each orientation (Elder Scrolls: Blades, GDC: dual orientation added 20 to 60% to UI design time; plan it from the start).
3. Touch
input_devices/pointing/emulate_mouse_from_touchis on by default: the first finger also arrives as a mouse, so mouse-only code works with one finger and never with two.- Track fingers by
InputEventScreenTouch.IndexandInputEventScreenDrag.Index. - Godot 4.7's buttons track a touch index, so two
Buttons can be held at once, for example move and jump (changelog GH-110893; not tried on a device). TouchScreenButton(a Node2D with anAction,PassbyPress, and shown on touchscreens only) still suits on-screen game controls.- Virtual stick, if the game needs one: write a Control (in the group
touch_control) that takes one touch index inside its rectangle and maps the offset toInput.ActionPress(action, strength). Or use MarcoFazioRandom's MIT "Virtual Joystick" add-on (record it inassets.json). - Android gestures (
InputEventMagnifyGesture,InputEventPanGesture) needenable_pan_and_scale_gestures. Turn onemulate_touch_from_mouseto try touch code with a mouse. - Test touch like any input (
input):Input.ParseInputEvent(new InputEventScreenTouch { Index = 1, Position = p, Pressed = true }), then wait a frame. Use two indices for two fingers.pis in window pixels, and a headless window is 64x64: with a stretch mode, (100, 420) reached_Inputas (1000, 4120). SendGetTree().Root.GetFinalTransform() * viewportPoint. - Game controls as hit pads (a GDC talk on Warhammer 40,000: Carnage): a line splits the screen, controls below and the game above. Draw small buttons, but give each a much larger invisible pad that reaches past where the largest thumb lands, so a thumb below the line never lands on nothing. In an overlap the touch goes to the pad it is nearest relative to the pad's size; the action that hurts most to miss (jump) gets the largest pad.
- A finger that slides into another pad switches to it without lifting; one that drifts off every pad keeps its pad. An
action stays held while any finger is on its pad. Release all on
NotificationApplicationPausedand focus loss. - Fade the drawn buttons after a few seconds of play (never to zero); offer a pad-size setting; give screens under
7 inches (where the thumbs meet) a tighter layout. Tested:
${CLAUDE_PLUGIN_ROOT}/examples/csharp/src/Example.Core/Controls/TouchPads.cs(DeadSpotslists where a thumb's reach finds no pad) and the C# example'sControls/TouchControls.cs. - Touch targets: 48 dp minimum (above). Xbox's accessibility guidelines ask for 15 mm on phones (
ui-accessibility).ScreenGetDpi()on Android is a bucket (160, 240, 320, 480...), so dp is exact and millimetres are approximate.
Controls the thumb finds without looking (a GDC talk, ${CLAUDE_PLUGIN_ROOT}/docs/gdc/controls-you-can-feel.md):
- A practised player knows what a press did before the screen shows it. Glass gives no such feel, so a control that is only nearly certain fails; on-screen feedback cannot make up for it. A click sound on each press helps.
- 48 dp is for buttons the player looks at. Buttons pressed blind (move, jump, fire): about 75 dp each, few of them,
each on or off. Set
minControlDpto 75. In-game things the player taps: never under about 40 dp (the runner cannot measure moving objects; test them in a flow). - A thumb lands within about 44 points, so a virtual stick, fixed or floating, gives many possible directions: the weak option. Prefer two or three big buttons, a tap on one of a few large choices, or gestures that hint rather than steer.
- Let the controls change the design: EA's Tetris replaced dragging with a tap on one of four drop spots, and made the thinking happen under time pressure while the input did not.
- Swipe up, not down (swiping down is uncomfortable); avoid moves of a few points (10-point Tetris columns).
4. Battery, heat and the app lifecycle
- Cap the frame rate (
application/run/max_fps, 30 for slow games) rather than relying onlow_processor_mode, which only helps apps that rarely redraw. display/window/energy_saving/keep_screen_onis on by default; turn it off for idle games and menus.- Android frame pacing (Swappy) is on by default.
- Godot 4.7 has no thermal API (Android's thermal headroom needs a plugin).
- Save on
NotificationApplicationPaused(Android and iOS only). iOS gives about 5 seconds and then kills the app (saves). Desktops, Steam Deck and consoles send nothing before a suspend: save often there instead (console-cert). Pause the game there and resume onNotificationApplicationResumed. OS.GetMemoryInfo()gives physical, free and available memory for telemetry. Godot has no memory budget API.- Lights per mesh. Mobile lights each mesh instance with at most 8 OmniLights and 8 SpotLights, a limit fixed in its
shader, not a setting; Compatibility's are
rendering/limits/opengl/max_lights_per_object(8) andmax_renderable_lights(32). Lights past the limit silently skip that mesh: split large floors so nearby lights stay under it, and bake the rest (LightmapGI).
5. Android export
- Tools: JDK 17 or newer (21 worked), and the SDK's
platform-tools,build-tools;35.0.1andplatforms;android-35. The NDK and CMake were not needed for APK or Gradle builds. - Editor settings:
ANDROID_HOMEandJAVA_HOMEonly seed new editor settings. An existingeditor_settings-4.7.treswins ("Invalid Android SDK path"). In CI, pointXDG_CONFIG_HOMEat an empty folder (Linux), or writeexport/android/android_sdk_pathandjava_sdk_pathinto it. SetDOTNET_NOLOGO=1: .NET's first-run banner was read as the target framework. - Required:
rendering/textures/vram_compression/import_etc2_astc=true(without it the export refuses), and a project icon (without one, the build uses Godot's icon).doctorchecks both. - Signing:
GODOT_ANDROID_KEYSTORE_RELEASE_PATH,_USERand_PASSWORD(the keystore and key passwords must match). Google Play wants an AAB, which needs a Gradle build:--install-android-build-template, thengradle_build/use_gradle_build=trueandgradle_build/export_format=1. It uses about 1.2 GB of Gradle cache, and Maven Central can answer 429 on shared runners, so retry once. No keystore yet:export -Preset Android -DebugBuildmakes a debug APK for a first run on a device (without one,exportfails withexport-no-keystore). - Google Play: new apps and updates must target API 36 since 31 August 2026; 4.7's default target is 36, minimum 24.
From 1 February 2027, updates must support 16 KB memory pages. 4.7.2's libraries passed
zipalign -c -P 16. - Launch tests need a device or an emulator with KVM (GitHub's Linux runners have it); none ran here.
6. iOS, in concept
macOS and Xcode only. application/min_ios_version defaults to 15.0. The preset's privacy/* options fill
PrivacyInfo.xcprivacy, so declare what the game collects (legal). The home indicator and status bar are hidden
by default. ScreenSetOrientation works only when the orientation setting is sensor.
7. Web builds (GDScript)
- Renderer: Compatibility only (WebGL 2); Forward+ and Mobile do not run, and there is no WebGPU. Safari has WebGL 2 issues.
- Threads: keep Thread Support off (the default since 4.3). A threads build shows "missing features: Cross-Origin
Isolation, SharedArrayBuffer" unless the server sends COOP and COEP headers, and itch.io and GitHub Pages send none.
The PWA option "Ensure Cross Origin Isolation Headers" works around it, but needed a second reload here.
doctorwarns about threads builds. - Size: the engine alone is 38 MB, 9.9 MB gzipped (GitHub Pages compresses; itch.io does not).
exportreportsexports[].web(rawMb,gzipMb), andexports[].size.packwhat the game adds, by folder and kind of file (byFolder,byType). Web portals cap downloads; CrazyGames, for example, allows 50 MB, or 20 MB for its mobile homepage. - Export outside the project. An export into
res://build/...gets imported, and the next export packs it (the.pckgrew tenfold here).exportadds a.gdignoreto an output folder inside the project;doctorwarns about presets that export into it. - Saves:
user://is IndexedDB, and it survived a reload. The write to IndexedDB happens a frame after the file closes, so callJavaScriptBridge.force_fs_sync()after a save that must survive closing the tab. Private windows do not keep it. - Audio needs a click or key press first in most browsers (not reproducible in automated Chromium). Bus effects do
not run on the web (
game-audio). - Input: fullscreen and mouse capture work only from inside an input event. Gamepads appear after their first button press. A background tab pauses the game.
- Test in browsers:
export -Preset Web -Run -Browser chromium,firefox,msedgeexports a debug build too and opens it in each browser. Release builds hide script errors: a call on null does nothing and the game carries on. The run fails (web-run-failed) when the engine does not start, or on any page error, failed request orERROR/SCRIPT ERRORline. Without the browser it fails withno-browser;npx playwright install --with-deps firefoxinstalls one.chromeandmsedgeuse the browsers installed on the machine. On a machine without a GPU (GitHub's runners) headless Firefox has no WebGL 2 and fails at start: give it a window,HYPERDOT_WEB_HEADED=1with Xvfb running (the kit's self-test does this).
8. Done means
- [ ]
layoutpasses on every profile the game ships for, with the real notch insets - [ ] two-finger input tested (
ParseInputEventwith two indices); every on-screen control is at least 48 dp, and game controls pressed without looking at least 75 dp (minControlDp) - [ ] on-screen game controls: no dead spot under a thumb, sliding between pads, everything released on pause
- [ ] the game saves on pause and resumes cleanly; the frame rate is capped where it can be
- [ ] Android:
doctorclean; an AAB signed; target API 36 - [ ] Web:
export -Run -Browser chromium,firefox,msedgepasses; the gzipped size fits the portal; a single-threaded build for itch.io
04 同仓库更多 Skills
over-budget
Use when a fixture exceeds the size budget.
bad-yaml
暂无简介
bad-workflow-description
Use when testing; runs the kit and then reads the JSON.
bad-version
Use when a fixture quotes a stale package version.
bad-value
Use when a fixture passes a value outside a parameter's set.
bad-trigger
Helps with Godot tests.