Porting an SDL game on Android (1/2) | Blog | Superflous Returnz

Porting an SDL game on Android (1/2) | Blog | Superflous Returnz Porting an SDL game on Android (1/2) | Blog | Superflous Returnz
  • Steam
  • Nintendo Switch
  • Itch.io
  • Google Play
Superfluous and his assistant Sophie

Blog

Porting an SDL game on Android (1/2)

2021-10-15

Very early in the development of the game, I had the idea of porting it to Android: the game style (point and click) fits the platform, for example I really enjoyed playing Thimbleweed Park on tablet (and being able to play old LucasArts on your phone via ScummVM is a pleasure too).

This is one of the reasons that lead me to switch from SDL1 to SDL2 which, in addition to providing welcome hardware acceleration to display high resolution images and animations, offers Android support (and even provides quite a bit of tools to manage this platform, we'll come to that).

Prerequisites

First, we need to install two things:

  • the Android SDK (Software Development Kit), which allows us to generate Android applications from Java sources (and a whole bunch of other things I will also talk about);
  • the Android NDK (Native Development Kit), which allows us to integrate code in another language such as C++ in my case.

I will not give a tutorial for these two points, you'll find many on the Internet. Note that Google heavily promotes Android Studio, which is a specialized IDE for Android. If you're like me, you'd rather be given scripts and left alone on how you want to program, you can also find the SDK on its own (in the Command line tools only). Same for the NDK.

Once you've installed these two components, you can get started.

An important point: in general, when we develop a program on Linux or Windows, we first seek to compile the program. The β€œdistribution” part (the icons, the installation program, etc.) comes only afterwards and is independent.

For Android, it works quite differently: an APK file (which you get when you install an app on Android) is not a simple executable (like an EXE on Windows) but a full software packaged and ready to be installed in one single click. Therefos, an Android project will necessarily include a lot of things that you will not always see in software repositories on other platforms: the icons again, but also the metadata, the description of the software, the translations of the title in different languages, etc.

First good news: the SDL provides us with a project skeleton ready to be used. This is the android-project folder which is included in SDL2 sources. This skeleton contains in particular the β€œglue” between your code and the hardware managed by Android, which is a hell of a job that we would not want to have to do by hand ... In short: the Java part is already done, we will hardly have not to touch it, all we have to do is plug in our C++ code, our data and all the rest.

What's in an APK?

Let's take a look at the tree structure of this android-project:


android-project
β”œβ”€β”€ app
β”‚Β Β  β”œβ”€β”€ build.gradle
β”‚Β Β  β”œβ”€β”€ jni
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ Android.mk
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ Application.mk
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ CMakeLists.txt
β”‚Β Β  β”‚Β Β  └── src
β”‚Β Β  β”‚Β Β      β”œβ”€β”€ Android.mk
β”‚Β Β  β”‚Β Β      └── CMakeLists.txt
β”‚Β Β  β”œβ”€β”€ proguard-rules.pro
β”‚Β Β  └── src
β”‚Β Β      └── main
β”‚Β Β          β”œβ”€β”€ AndroidManifest.xml
β”‚Β Β          β”œβ”€β”€ java
β”‚Β Β          β”‚Β Β  └── org
β”‚Β Β          β”‚Β Β      └── libsdl
β”‚Β Β          β”‚Β Β          └── app
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ HIDDeviceBLESteamController.java
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ HIDDevice.java
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ HIDDeviceManager.java
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ HIDDeviceUSB.java
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ SDLActivity.java
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ SDLAudioManager.java
β”‚Β Β          β”‚Β Β              β”œβ”€β”€ SDLControllerManager.java
β”‚Β Β          β”‚Β Β              └── SDL.java
β”‚Β Β          └── res
β”‚Β Β              β”œβ”€β”€ mipmap-hdpi
β”‚Β Β              β”‚Β Β  └── ic_launcher.png
β”‚Β Β              β”œβ”€β”€ mipmap-mdpi
β”‚Β Β              β”‚Β Β  └── ic_launcher.png
β”‚Β Β              β”œβ”€β”€ mipmap-xhdpi
β”‚Β Β              β”‚Β Β  └── ic_launcher.png
β”‚Β Β              β”œβ”€β”€ mipmap-xxhdpi
β”‚Β Β              β”‚Β Β  └── ic_launcher.png
β”‚Β Β              β”œβ”€β”€ mipmap-xxxhdpi
β”‚Β Β              β”‚Β Β  └── ic_launcher.png
β”‚Β Β              └── values
β”‚Β Β                  β”œβ”€β”€ colors.xml
β”‚Β Β                  β”œβ”€β”€ strings.xml
β”‚Β Β                  └── styles.xml
β”œβ”€β”€ build.gradle
β”œβ”€β”€ gradle
β”‚Β Β  └── wrapper
β”‚Β Β      β”œβ”€β”€ gradle-wrapper.jar
β”‚Β Β      └── gradle-wrapper.properties
β”œβ”€β”€ gradle.properties
β”œβ”€β”€ gradlew
β”œβ”€β”€ gradlew.bat
└── settings.gradle

In principle, you shouldn't need to change the Java files in app/src/main/org/libsdl/app which contain, as I said, the β€œglue” between your C++ code and the SDL port on Android .

Files with gradle in the name are the Android build system configuration files (Gradlew). Usually they are ready to use, we just need to modify a few variables. For example, in app/build.gradle, we find the following line:


applicationId "org.libsdl.app"

That we will take care to replace by the identifier of our application, here:


applicationId "net.ptilouk.superfluous"

This identifier will also be the path to the file where the main Java class of our application will be located, and which must be created. Here, it will therefore be called app/src/main/java/net/ptilouk/superfluous/superfluous.java and will contain just a few lines:


package net.ptilouk.superfluous;

import org.libsdl.app.SDLActivity;

public class superfluous extends SDLActivity { }

Likewise, we indicate the identifier of the application and the name of the activity (the class which inherits from SDLActivity) in the fileapp/src/main/AndroidManifest.xml, as well as the version numbers that will allow Android to detect if the software needs to be updated:


package="net.ptilouk.superfluous"
android:versionCode="101000"
android:versionName="1.1.0-modern-ui"

<activity android:name="superfluous"

The PNG images contained in the app/src/main/res/mipmap-* folders are the application icons in different resolutions and can thus be replaced by our own:

icons

The app/src/main/res/values folder contains strings, including the name of the application. To manage translations, we can add similar folders by adding the language code to the folder name, for example value-fr for French in my case.

So app/src/main/res/values/strings.xml contains:


<resources>
    <string name="app_name">Superfluous Returnz</string>
</resources>

While app/src/main/res/values-fr/strings.xml contains:


<resources>
    <string name="app_name">Superflu Riteurnz</string>
</resources>

(I know, the difference in names is subtle.)

Compiling C ++ with third-party libraries

We use the .mk (Makefile) files to configure our C++ application. The one that runs our C++ application is app/jni/src/Android.mk and originally contains this:


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := main

SDL_PATH := ../SDL

LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include

# Add your application source files here...
LOCAL_SRC_FILES := YourSourceHere.c

LOCAL_SHARED_LIBRARIES := SDL2

LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog

include $(BUILD_SHARED_LIBRARY)

First of all, we want to add our source files and our includes which are in a folder stored in the variable SRC_PATH, so we add this information:


SRC_PATH := ../../../../../

LOCAL_C_INCLUDES := \
  $(LOCAL_PATH)/$(SRC_PATH)/include \
  $(LOCAL_PATH)/$(SDL_PATH)/include \

LOCAL_SRC_FILES := \
  $(wildcard $(LOCAL_PATH)/$(SRC_PATH)/src/*.cpp) \
  $(wildcard $(LOCAL_PATH)/$(SRC_PATH)/src/*/*.cpp) \
  $(wildcard $(LOCAL_PATH)/$(SRC_PATH)/src/*/*/*.cpp)

Let's also add the C++ flags we need:


LOCAL_CPPFLAGS := -std=c++17 -frtti -O3 -DNDEBUG \
                  -DSOSAGE_DATA_FOLDER=\"data/\"

We will thus use C++17, activate the maximum compiler optimizations and deactivate the debugging information (we can remove this while we are developing, of course).

The -frtti flag is a technical detail: by default, the NDK disables Run-Time Type Information (RTTI) (via dynamic_cast<>() in C++). As I use this quite heavily in my code, I reactivate it with this flag.

The variable SOSAGE_DATA_FOLDER is used to indicate where the game data is located. On Gnunux, for example, this will be in /usr/share/superfluous-returnz/. On Android, asset management is rather particular, because the program "sees" a virtual folder which will specifically contain the game data and which corresponds to the app/src/main/assets/ folder . So here, we're going to copy our data to app/src/main/assets/data/ and we just indicate data/ for the folder.

About dependencies, SDL is of course already included, but it lacks the additional SDL components that I use, as well as two other third-party libraries, Yaml and LZ4. Let's add them.

The full file now looks like this:


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := main

SDL_PATH := ../SDL
SDL_IMAGE_PATH := ../SDL_image
SDL_MIXER_PATH := ../SDL_mixer
SDL_TTF_PATH := ../SDL_ttf
YAML_PATH := ../yaml
LZ4_PATH := ../LZ4
SRC_PATH := ../../../../../

LOCAL_CPPFLAGS := -std=c++17 -frtti -O3 -DNDEBUG

LOCAL_C_INCLUDES := \
  $(LOCAL_PATH)/$(SRC_PATH)/include \
  $(LOCAL_PATH)/$(SDL_PATH)/include \
  $(LOCAL_PATH)/$(SDL_IMAGE_PATH)/include \
  $(LOCAL_PATH)/$(SDL_MIXER_PATH)/include \
  $(LOCAL_PATH)/$(SDL_TTF_PATH)/include \
  $(LOCAL_PATH)/$(YAML_PATH)/include \
  $(LOCAL_PATH)/$(LZ4_PATH)/include \

LOCAL_SRC_FILES := \
  $(wildcard $(SRC_PATH)/src/*.cpp) \
  $(wildcard $(SRC_PATH)/src/*/*.cpp) \
  $(wildcard $(SRC_PATH)/src/*/*/*.cpp)

LOCAL_SHARED_LIBRARIES := SDL2 SDL2_image SDL2_mixer SDL2_ttf yaml lz4

LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog

include $(BUILD_SHARED_LIBRARY)

We copy the sources (or make a symlink if we prefer) of the different SDL components, Yaml and LZ4 to the specified folders. The various SDL components directly provide Android.mk files for compilation to Android, but this is not the case for Yaml and LZ4. Well okay, let's just write them down! We can largely draw inspiration from what was done for SDL.

For Yaml:


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := yaml

LOCAL_C_INCLUDES := $(LOCAL_PATH)/libyaml/include $(LOCAL_PATH)/libyaml/src

LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/libyaml/src/*.c)

LOCAL_EXPORT_C_INCLUDES += $(LOCAL_C_INCLUDES)

LOCAL_CFLAGS := \
  -DYAML_VERSION_MAJOR=0 \
  -DYAML_VERSION_MINOR=2 \
  -DYAML_VERSION_PATCH=2 \
  -DYAML_VERSION_STRING=\"0.2.2\"

include $(BUILD_SHARED_LIBRARY)

For LZ4, almost the same thing:


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := lz4

LOCAL_C_INCLUDES := $(LOCAL_PATH)/lz4/lib

LOCAL_SRC_FILES := $(wildcard ${LOCAL_PATH)/lz4/lib/*.c)

LOCAL_EXPORT_C_INCLUDES += $(LOCAL_C_INCLUDES)

include $(BUILD_SHARED_LIBRARY)

There it is! We can now compile our application with a single command, in the root folder:


./gradlew assemble

To be continued

We now have an application that compiles, and that's a good start ... but now, how do we manage Android on the code side? We'll discuss that next time!

i