Mobile: Android and iOS
MotaEngine games run on Android phones and iPhones from the same project you
build for Windows: the same scenes, Lua scripts and C++ game module. Mobile
packages are built from the engine source (the prebuilt Windows and Linux
SDKs do not include Android or iOS libraries; mota package -Platform android|ios from an SDK is refused). You need a checkout of the engine source
from the motaengine.com portal.
| Android | iOS | |
|---|---|---|
| Devices | Android 8.0 (API 26) or later, arm64, a Vulkan 1.3 GPU (most phones from 2022 on) | iOS 15 or later (iPhone / iPad), arm64 |
| Graphics | Vulkan | Vulkan through MoltenVK (Metal) |
| Build host | Windows (this checkout) | a Mac with Xcode (the Windows side can drive it over ssh) |
| Packages | APK (testing), AAB (Google Play) | .app (device testing), .ipa / direct upload (TestFlight, App Store) |
| Store tools | tools/play_upload.py (Play Developer API) | tools/package_ios.sh --export app-store-connect --upload |
The engine's own samples prove both: Ember Run is on Google Play internal
testing and TestFlight; LibertySlice, StormForge, Beacon Hold and mota new
games run on a Galaxy A17 and an iPhone 13 Pro.
1. Project settings for a mobile game
All in the .motaproj settings (the editor's Project Settings edits them):
| Setting | Meaning |
|---|---|
bundleId | Store id, e.g. com.yourstudio.game (Android application id and iOS CFBundleIdentifier). Required for release builds; com.example.* is refused by Google Play. tools\mota new MyGame -BundleId com.yourstudio.mygame sets it. |
displayName | Name under the icon (empty = the project name). |
version | 1.2.0. The build number / version code is derived automatically (minutes since 2025-01-01 UTC), so every upload is higher. |
appIcon | One square PNG, 1024 px, opaque, full-bleed (no rounded corners: the launcher / iOS masks it). Empty = the MotaEngine icon. Android gets adaptive + legacy icons and a 512 px Play listing icon; iOS an asset catalog. |
orientation | landscape (default, both sides), portrait, or any. |
encryption | false for a single-player game: no encryption code at all, so the App Store export-compliance answer is "none" (the iOS Info.plist declares ITSAppUsesNonExemptEncryption = NO). Online play is unavailable in such a build. Default true. |
mobileTargetFps | Frame cap on phones: 0 (default) = the device tier's (30 on Low and Medium - every iPhone, most Android phones - 60 on High), or 30 / 60. |
2. Android
One-time setup (Windows)
- Android Studio (for the SDK, platform tools and its bundled JDK 17).
In SDK Manager > SDK Tools install NDK (Side by side) 27.x. The driver
finds the SDK at
%LOCALAPPDATA%\Android\Sdk(or$env:ANDROID_HOME), the NDK under<sdk>\ndk\27.*(or$env:ANDROID_NDK_HOME) and the JDK through$env:JAVA_HOMEor Android Studio's JBR. - Visual Studio 2026 with its CMake, Ninja and vcpkg components (the same
install the Windows build uses).
$env:VCPKG_ROOTmay point at another vcpkg clone;$env:MOTA_CMAKEat another CMake >= 3.25. - A phone with USB debugging on (Settings > About > tap Build number 7
times; Developer options > USB debugging), connected and authorized
(
adb deviceslists it).
The first package build installs the engine's Android dependencies (about 20
vcpkg ports for arm64) into build-android-installed\ in the checkout. That
takes a while once; later builds reuse it.
Build, install, run
cd MyGame # a folder holding the .motaproj
..\tools\mota package -Platform android -BuildType debug
# -> dist\MyGame\android\MyGame-1.0.0-arm64-v8a-debug.apk
adb install -r dist\MyGame\android\MyGame-1.0.0-arm64-v8a-debug.apk
adb shell monkey -p com.yourstudio.mygame -c android.intent.category.LAUNCHER 1
adb logcat -s Motaware # engine bootstrap messages
adb exec-out run-as com.yourstudio.mygame cat files/Saved/Logs/Engine.log # debug builds only
-BuildType release builds the Shipping configuration (optimized, LTO,
stripped). Other switches pass through to tools/package_android.ps1:
-Artifact aab (a Play bundle), -Abis arm64-v8a,x86_64 (add the emulator
ABI), -SkipCook / -SkipNative (reuse the last pak / native library).
A C++ game with its own CMakeLists.txt (what mota new creates) is compiled
into the app's native library: mota_setup_game_target(<Game>) must stay the
last call for the game target and main() must live in main.cpp (it is
compiled in and runs on the engine thread exactly as on desktop). A
script-only game uses the engine's own app.
Release signing and Google Play
Create an upload key once and keep it backed up, outside version control:
& "$env:JAVA_HOME\bin\keytool.exe" -genkeypair -v -keystore platform\android\upload.jks `
-alias upload -keyalg RSA -keysize 2048 -validity 10000
and platform\android\keystore.properties (both are gitignored):
storeFile=upload.jks
storePassword=...
keyAlias=upload
keyPassword=...
Without it, release builds are signed with the debug key: installable, not uploadable. Then:
..\tools\mota package -Platform android -Artifact aab
# -> dist\MyGame\android\MyGame-1.0.0-arm64-v8a-release.aab
# + ...-release-native-debug-symbols.zip (readable native crash reports in Play Console)
# + MyGame-play-icon-512.png (the store listing icon)
In Play Console create the app and upload its first AAB by hand (the API cannot create apps). For later uploads, create a Google Cloud service account, invite it in Play Console > Users and permissions with "Release to testing tracks", download its JSON key, and:
$env:MOTA_PLAY_KEY = "C:\keys\play-service-account.json"
python tools\play_upload.py com.yourstudio.mygame --dry-run # access check, changes nothing
python tools\play_upload.py com.yourstudio.mygame dist\MyGame\android\MyGame-1.0.0-arm64-v8a-release.aab --track internal --notes "What's new"
The native-symbols zip beside the AAB is attached automatically.
--track production also needs --allow-production.
3. iOS
One-time setup (Mac)
- Xcode (26 or later) from the App Store; open it once, sign in with your Apple Developer account (Settings > Accounts) and accept the licence.
- Homebrew
cmakeandninja(brew install cmake ninja). - vcpkg:
git clone https://github.com/microsoft/vcpkg ~/vcpkg && ~/vcpkg/bootstrap-vcpkg.sh(VCPKG_ROOTdefaults to~/vcpkg). - The engine source on the Mac (the same release as on Windows), e.g.
~/Documents/GameEngine. - Your Apple Team ID (developer.apple.com > Membership), and for the App Store the app's bundle id registered with an app record in App Store Connect.
- An iPhone with Developer Mode on (Settings > Privacy & Security), connected and trusted.
The first build installs the iOS dependencies into build-ios-installed/
(once, then cached).
Build from Windows over ssh
With ssh access to the Mac (key login), mota cooks the pak on Windows and
builds on the Mac:
$env:MOTA_MAC_HOST = "you@your-mac.local" # engine checkout at ~/Documents/GameEngine ($env:MOTA_MAC_REPO)
..\tools\mota package -Platform ios -BuildType debug
Code signing needs the Mac's login keychain, which an ssh session cannot use:
an unsigned build works over ssh; for a signed one, run the command mota
prints in a Terminal window on the Mac.
Build, sign and upload on the Mac
cd ~/Documents/GameEngine
tools/package_ios.sh --project ~/games/MyGame/MyGame.motaproj --pak ~/MyGame.pak \
--sdk iphoneos --config Shipping --team <TEAM_ID>
# -> dist/MyGame/ios/MotawareGame.app ; install: xcrun devicectl device install app --device <UDID> <path>.app
# TestFlight / App Store: archive, export and upload in one go
tools/package_ios.sh --project ~/games/MyGame/MyGame.motaproj --pak ~/MyGame.pak \
--icon ~/MyGame-AppIcon.png --sdk iphoneos --config Shipping --team <TEAM_ID> \
--export app-store-connect --upload
--upload uses the Apple account signed in to Xcode; without it the .ipa
lands in dist/MyGame/ios/ for Transporter.app. The archive carries the dSYM,
so TestFlight and App Store crash reports are symbolicated. The pak comes from
MotaEngine.exe --package=<proj> (or mota package on Windows).
4. How games behave on phones
- Touch: a virtual gamepad (stick, A/B/X/Y, pause) appears over the HUD;
bind
PadAxis/Padsources to use it. Other touches act as the mouse (tap = click, drag = mouse movement).--touch-controlspreviews it on desktop. - Screen: full-screen, edge to edge. Android hides the system bars (a swipe shows them for a moment). Menus and the HUD stay inside the safe area (camera cut-out, iPhone notch and home indicator). The screen stays on while the game is in the foreground.
- Back button (Android): pops a menu or pauses (it acts as Escape / B); on a root menu with no back action (the title screen) it moves the app to the background, as Android expects.
- Quitting (iOS): iOS apps never quit themselves.
GameContext::canQuit(C++) /mota.canQuit()(Lua) are false there - add Quit / Quit to Desktop menu entries only when they are true. The player leaves with the home gesture. - Background: when the app is backgrounded or interrupted (a call, the lock button) simulation, rendering and audio pause; they resume when it returns.
- Saves: under the app's private storage (
GameContext::savedDirectory); they survive app updates, and every write is atomic, so an app killed in the background never leaves a half-written save. - Frame rate and heat: frames are capped and paced (30 fps by default, see
mobileTargetFps). When the phone reports a Serious / Critical thermal state the cap drops to 30 / 15 until it cools. - Render tiers: Android defaults to Low, iOS to Medium (
--mobile-tier=overrides); desktop-only features (Lumen, virtual shadow maps, ray tracing, mesh shaders) are off and the GPU memory they would take is not allocated. - Sound: iOS uses the Ambient audio session, so the ring / silent switch mutes the game (as for most games).
5. Testing and diagnostics
- Smoke matrix:
tools\mobile_smoke.ps1 -Platform android -Projects MyGame\MyGame.motaprojbuilds, installs and runs each project on the connected phone and checks its log.-Platform iosprepares the same on the Mac and prints the Terminal command to run there. - Frame stats: launch with
--frame-stats(orstat.frames 5in the console) to log fps, frame-time percentiles, hitches, CPU time and the thermal state. On Android, pass engine arguments withadb shell am start -n <bundleId>/com.hypermatrix.engine.MotaPalActivity --esa args '--game,--pak=/data/user/0/<bundleId>/files/<Game>.pak,--frame-stats=5'(debug builds); on iOS,xcrun devicectl device process launch --device <UDID> <bundleId> --frame-stats=5. - GPU memory:
r.gpumemlists device memory by allocation site. - Crash reports: a crash writes
Saved/Crashes/crash_<time>.log+.jsonwith a backtrace (the next launch logs a summary;crash.list/crash.drainhandle the queue), and the store consoles still receive it: Play Console > Android vitals (symbolicated with the uploaded native-symbols zip) and App Store Connect / Xcode Organizer (with the dSYM). - Logs: Android debug builds:
adb exec-out run-as <bundleId> cat files/Saved/Logs/Engine.log; iOS:xcrun devicectl device copy from --device <UDID> --domain-type appDataContainer --domain-identifier <bundleId> --source Documents/Motaware/Saved/Logs/Engine.log --destination Engine.log.
More detail: platform/android/README.md, platform/ios/README.md,
docs/android_renderer_notes.md, docs/ios_renderer_notes.md.
