Blog
Porting an SDL game on Android (1/2)
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:
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!