Nikl.me

Notes on mobile development with Bevy #2

My Bevy mobile app is very incomplete, but I would like to be able to distribute test builds early on. That means Android builds need to be acceptable for the Play Store and iOS builds need to pass Apple's bar for the App Store. On the way to that goal, there are a couple technical issues to solve and some configuration to do in App Store Connect and Google Play Console. I will concentrate on the technical challenges while mentioning some store configurations.

Most of this effort did not directly go into my current app project. I decided to add iOS and Android support to bevy_game_template first. This way, the community might benefit more from my work and I can just copy the setup for other projects.

The starting point of this post is the mobile example from Bevy with small adjustments discussed in a previous post.

Android

Getting an Android App Bundle

The cargo-apk setup from Bevy's mobile example allows us to build APK files. The Play Store no longer accepts APKs, but requires app bundles. This makes sense to keep downloads small and still support multiple device ABIs (Application Binary Interface), but it requires some extra work to get an app bundle for a Bevy game.

The only tool I could convince to create an app bundle from my Bevy project is xbuild, the WIP successor of cargo-apk1. We can configure xbuild to bundle our project for Android with the following manifest.yaml

android:
  gradle: true
  icon: "your_icon.png"
  manifest:
    package: "com.example.bevygame"
    version_code: 1
    application:
      label: "Bevy game"

Currently, xbuild decides to create app bundles only when using gradle: true and only for production builds. Running x build --platform android --store play creates an app bundle, but derived APKs instantly crash.

I had to do a couple changes to get xbuild to create working app bundles. Some of them might get upstreamed, but a few are obvious hacks that only work for the Bevy project structure (like a single, hardcoded assets directory). For the xcode commands in this post to work as expected, you need to install xbuild from my fork: cargo install --git https://github.com/NiklasEi/xbuild.

libc++ shared is missing

Looking at the logs with adb logcat while trying to start the app on my phone yields the following error:

java.lang.UnsatisfiedLinkError: Unable to load native library "/data/app/~~0cxWIprct-rKy7Vggu9E4g==/me.nikl.bevygame-YTVT_qxOwHzm_kGsGV9cYw==/lib/arm64/libmobile.so": dlopen failed: library "libc++_shared.so" not found

The helpful line among hundreds of unhelpful ones.

Opening the APK as an archive indeed shows that lib/arm64-v8a/ does not include libc++_shared.so, while an APK built with cargo-apk includes it. This library is required for audio support in Bevy. I probably could have just ripped out audio for now and moved on, but who am I kidding? I want audio!

Telling gradle to include libc++_shared.so turned out to be more complicated than I had thought. Usually, it should realize on its own that the library is needed and include it. The problem is, that we are not using gradle to build the game library, but cargo. So gradle has no way of knowing that we need libc++_shared.so. In the end, I gave up on a "clean" solution and went with a hack. I basically told gradle that the project includes a c++ library which requires the shared library. The c++ project is empty, but it does the trick and creates minimal bloat (~4kB).

With this change, the UnsatisfiedLinkError from above is gone, but the game still doesn't work.

Include assets

While cargo-apk allows simply configuring an asset directory, xbuild does not jet support that. There is an open PR to add support for assets to the non-gradle builds, but that only works for APKs2.

Since Bevy usually comes with a single assets directory in the root of the project, I told gradle to just include that with a hardcoded path from deep inside the target directory. At this point, the build generated by xbuild using gradle runs properly.

Continuous deployment

The app bundle needs to be signed with jarsigner and can then be uploaded to Google Play Console. Doing this manually for every version is no fun, so I wrote a GitHub workflow for it.

Support more Android devices

After uploading an app bundle, we can use the app bundle explorer in the Google Play Console to check bundle exports for different devices. My early bundles contained libraries for 4 different ABIs due to the dummy c++ library and gradle compiling for all non-deprecated ABIs by default. The game, on the other hand, was only compiled for arm64-v8a, which was hardcoded in xbuild.

The device list in the app bundle explorer can be filtered by ABI. Out of the 18.919 devices, 8.599 are arm64-v8a and 10.096 armeabi-v7a. Most of the other couple hundred devices are x86 or x86_643. I went back to xbuild, fixed the support for arm in gradle builds and told it to always build for arm64 and arm when making a build with the --store option.

The bundle still contains lib directories for x86 and x86_64 due to the "dummy cpp lib" workaround. Those lib directories tell the play store that our app bundle can generate APKs for those ABIs which is not correct since we do not build our game for them. A small gradle configuration tells it to not build the dummy library for the x86(64) ABIs.

iOS

I don't own a Mac or iPhone, but still want to support iOS. So I borrowed a Mac for a couple of days to get everything set up in bevy_game_template.

The first time I tried to let Xcode publish the app with the copied setup from the Bevy mobile example, the process ended in a list of errors from the App Store. The required changes boiled down to adding an app icon and a launch screen.

Add Icons

The App Store requires apps for iOS 11 or later to include app icons with an asset catalog. I did this following the developer documentation from Apple (icon + name).

Adding a launch screen

Our app requires a launch screen. The Info.plist from the Bevy mobile example includes an entry for UILaunchStoryboardName, but the launch screen itself is not included. I created one in Xcode following a stack overflow answer that just contains a centered icon.

Continuous deployment

At this point, Xcode can create app builds and push them to App Store Connect. I spent some time on creating a GitHub workflow to automate the process. The iOS workflow requires a bit more setup than the Android one, but hopefully, I don't need to organize a Mac again anytime soon.

One issue left...

...the app has no audio on iOS. I have only tested this with bevy_kira_audio so far, so bevy_audio might be fine. Maybe someone else knows what's going on here?

Getting ready for a release

Both stores require some setup for the store pages. The app from bevy_game_template is not supposed to go live in the app stores, but it would be nice to be able to share a link that allows anyone to install it. For iOS the goal is "external testing" in AppFlight. In Google Play this release type is called "open testing". Both of these programs require the app to go through a review and the store page to be mostly complete.

Getting screenshots

Screenshots are a big part of finishing the required setup for publishing. Luckily, the last Bevy update came with a very handy screenshot API. Setting the window dimension and adding the below system gives nice screenshots in configurable resolution.

fn screenshot_on_spacebar(
    input: Res<Input<KeyCode>>,
    main_window: Query<Entity, With<PrimaryWindow>>,
    mut screenshot_manager: ResMut<ScreenshotManager>,
    mut counter: Local<u32>,
) {
    if input.just_pressed(KeyCode::Space) {
        let path = format!("./screenshot-{}.png", *counter);
        *counter += 1;
        screenshot_manager
            .save_screenshot_to_disk(main_window.single(), path)
            .unwrap();
    }
}

The system is borrowed from the Bevy screenshot example.


Thank you for reading! If you have any feedback, questions, or comments, you can find me at @nikl_me@mastodon.online or on the Bevy Discord server (@nikl).


  1. Crossbow might also work, but I didn't manage to set it up correctly. If you do, please tell me.
  2. It also doesn't support globs yet, which is a bit unpractical for Bevy's asset directory. Every file and subdirectory would need to be listed separately. I added simple glob support for APK builds on my fork and will try to upstream that once basic assets support has landed.
  3. Now I understand why the Bevy mobile example has the two arm targets configured for cargo-apk builds :D
Nikl.me © 2022