MotaEngine

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.

AndroidiOS
DevicesAndroid 8.0 (API 26) or later, arm64, a Vulkan 1.3 GPU (most phones from 2022 on)iOS 15 or later (iPhone / iPad), arm64
GraphicsVulkanVulkan through MoltenVK (Metal)
Build hostWindows (this checkout)a Mac with Xcode (the Windows side can drive it over ssh)
PackagesAPK (testing), AAB (Google Play).app (device testing), .ipa / direct upload (TestFlight, App Store)
Store toolstools/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):

SettingMeaning
bundleIdStore 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.
displayNameName under the icon (empty = the project name).
version1.2.0. The build number / version code is derived automatically (minutes since 2025-01-01 UTC), so every upload is higher.
appIconOne 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.
orientationlandscape (default, both sides), portrait, or any.
encryptionfalse 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.
mobileTargetFpsFrame 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)

  1. 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_HOME or Android Studio's JBR.
  2. Visual Studio 2026 with its CMake, Ninja and vcpkg components (the same install the Windows build uses). $env:VCPKG_ROOT may point at another vcpkg clone; $env:MOTA_CMAKE at another CMake >= 3.25.
  3. A phone with USB debugging on (Settings > About > tap Build number 7 times; Developer options > USB debugging), connected and authorized (adb devices lists 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)

  1. Xcode (26 or later) from the App Store; open it once, sign in with your Apple Developer account (Settings > Accounts) and accept the licence.
  2. Homebrew cmake and ninja (brew install cmake ninja).
  3. vcpkg: git clone https://github.com/microsoft/vcpkg ~/vcpkg && ~/vcpkg/bootstrap-vcpkg.sh (VCPKG_ROOT defaults to ~/vcpkg).
  4. The engine source on the Mac (the same release as on Windows), e.g. ~/Documents/GameEngine.
  5. 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.
  6. 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 / Pad sources to use it. Other touches act as the mouse (tap = click, drag = mouse movement). --touch-controls previews 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.motaproj builds, installs and runs each project on the connected phone and checks its log. -Platform ios prepares the same on the Mac and prints the Terminal command to run there.
  • Frame stats: launch with --frame-stats (or stat.frames 5 in the console) to log fps, frame-time percentiles, hitches, CPU time and the thermal state. On Android, pass engine arguments with adb 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.gpumem lists device memory by allocation site.
  • Crash reports: a crash writes Saved/Crashes/crash_<time>.log + .json with a backtrace (the next launch logs a summary; crash.list / crash.drain handle 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.