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

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

Blog

Porting an SDL game on Android (2/2)

2021-10-20

Last time, I discussed the whole technical aspect of “setting up” an Android environment to run our game: configuration, compilation, etc.

Now, we get to the heart of the matter: what does it change, on the code side, to port an SDL2 game on Android?

Debugging an Android app

When you code, you rarely get the result you want on the first try, there are always bugs to find and fix, and porting a game to Android will certainly be no exception! So of course, given that we are already debugging the problem on Linux, we limit the chances of having bugs on Android, but we are never safe from errors specific to one platform or another ...

So, just as it is easy to debug an application on a computer with messages on a console (or, better, a debugger), getting any messages from an Android device requires a little more work.

Fortunately, several tools are at our disposal. First of all, to avoid systematically having to use an external Android device (phone or tablet), we can use the emulator supplied with the SDK:

emulator

The adb software, also supplied with the SDK, allows you to easily install command line apps, either to the emulator or to a device connected to the computer. So, instead of having to manually transfer the APK to a tablet and then install it, we can simply do:


adb install --user 0 app/build/outputs/apk/debug/app-debug.apk

The --user 0 flag is optional and allows you to install the app only for the first user.

Small bonus: we can even connect directly to a tablet via the local Wi-Fi network, that way we don't even bother with a cable ... Then, we can retrieve the information sent to a sort of “console” by the various programs via adb logcat.

All this is good, but if in your code, you simply call std::cout or std::cerr calls, you will be disappointed: nothing will be displayed by adb logcat. The standard and error outputs are in fact not reflected in the Android log, so you have to go through a specific function, accessible via the #include <android/log.h> header. In order to be able to easily juggle between the usual outputs and the Android output, we can use a dedicated debug() function and change the definition at compilation, via the preprocessor, depending on the target system:


#if defined(SOSAGE_ANDROID)
inline void debug (const std::string& str)
{
  __android_log_print (ANDROID_LOG_INFO, "Sosage", "%s", str.c_str());
}
#else
inline void debug (const std::string& str)
{
  std::cerr << str << std::endl;
}
#endif

Note that we could even define a custom stream operator (to be able to do something like My_stream << " Debug " << std::endl), but I admit that I was a bit lazy ... That said, it is quite possible that I'll do it soon, because it would still be very practical!

A very useful thing: the second argument, here "Sosage", will be copied at the start of the line at each function call, which will allow us, by doing adb logcat | grep Sosage for example, to only retrieve the output of our program. Yes, because adb logcat displays all that is happening on your Android, and it can quickly get very verbose!

Accessing data

As I explained in the CMakeLists, Android does not allow apps to directly access the file system. Instead, you have to go through an asset manager: just as you can't use std::cerr to write info to the console, you can't use the usual std::ifstream/std::ofstream to read and write files.

Once again, SDL saves our lives by taking care of managing this aspect of Android: indeed, the SDL file management API (like SDL_LoadImage()) transparently switches from the classic file system on Linux et al to the_asset manager_ on Android (as well as other variants, one day I will tell you for example about_Emscripten_ which allows us to port the game to any browser). And as SDL offers, in addition to the functions dedicated to image reading, general functions to open / read / write files, we just need to use this API to get everything working fine on Android too! It's very simple, instead of doing:


std::ifstream my_file (SOSAGE_DATA_FOLDER + "/my_file.whatever");
int my_data;
my_file >> my_data;
my_file.close();

We'll do:


SDL_RWops* my_file = SDL_RWFromFile(SOSAGE_DATA_FOLDER + "/my_file.whatever");
int my_data;
SDL_RWread(my_data, &my_data, sizeof(int), 1);
SDL_RWclose(my_file);
// ...

The API is a bit more verbose and less modern (it's C), but we can easily wrap it all into a higher level API of our choosing. And there we go!

Touchscreen and Android events

Android means, in general, touchscreen. And another good news: the SDL provides everything we need to manage these events! Note that this is not specific to Android: you can very well use a touchscreen on another OS, and you can also connect a mouse to your tablet ...

Thus, we will mainly use 3 events:

  • SDL_FINGERDOWN, which we receive when a finger is placed on the screen;
  • SDL_FINGERMOTION, received when this finger moves;
  • SDL_FINGERUP, received when the finger leaves the screen.

It is in itself quite similar to the management of the mouse which has 3 equivalent events (SDL_MOUSEBUTTONDOWN, SDL_MOUSEMOTION and SDL_MOUSEBUTTONUP), with a little subtlety:

  • the mouse events are given with integer coordinates corresponding to the number of pixels traversed from the origin at the top left of the window. If I click in the middle of HD 1920x1080 window, then my program receives an ev event of type SDL_MOUSEBUTTONDOWN with the values ev.button.x == 960 and ev.button.y == 540;
  • for some reason, touch events give floating coordinates corresponding to the fraction of the window traversed from the origin at the top left. So, if I click in the middle of my window, my program receives an ev event of type SDL_FINGERDOWN with the values ev.button.x == 0.5 and ev.button.y == 0.5. So we just have to multiply these values respectively by the width and height of your window to fall back to the same values as with a click of the mouse, but you have to know it!

Another detail that can be quite annoying if, like me, you want to handle both the mouse and the touchscreen independently of the OS: touch events also generate ... false mouse events. It's a weird technical choice, but basically when you tap the screen, the program both receives a SDL_FINGERDOWN event AND a SDL_MOUSEBUTTONDOWN event. The same goes for the movement and the lifting of the finger. I guess if you don't want to worry about the input mode and handle the mouse exactly the same as the touchscreen, it can be very handy (you can ignore touch events altogether and interpret only mouse events) ... In my case, the game modes will be quite different, and this behavior is rather annoying.

I solved the problem by just checking, when I get a mouse event, if there isn't also a touch event at the same time, in which case I ignore the mouse event. Once again, nothing very complicated, but you have to know it ...

Finally, let's talk about some events specific to Android and which can be very handy:

  • an event of the type SDL_KEYDOWN (key pressed) with the value ANDROID_BACK corresponds to pressing the “return” key on your Android device: useful for, for example, displaying the game menu, skipping a cutscene, etc. I treat it a bit like the “Esc” touch on a keyboard;
  • SDL_APP_WILLENTERBACKGROUND is generated when the application is about to go to the background (for example, if the key to return to the Android home has been pressed): in my case, it allows me to put the game paused. We also have SDL_APPDIDENTERBACKGROUND which is generated when the application is effectively put in the background (the difference is subtle);
  • similarly, we have SDL_APP_WILLENTERFOREGROUND and SDL_APP_DIDENTERFOREGROUND to warn us that the app will come back (or has come back) to the foreground: in my case, it's the end of the pause, the game resumes normally ;
  • SDL_APP_TERMINATING which warns us that the application is stopping, and therefore lets us save what we need and cleanly exit the app (we can see this as the equivalent of pressing the cross in the desktop version, or the SDL_QUIT event).

With these different events, we can now manage all possible interactions with an average Android device!

Java interface access

A final technical point, quite optional but which can be useful: accessing the Java interface. Indeed, technically, the application is a Java program in which your C++ program runs. What the SDL gives us access to is already very comprehensive, but in reality, the Android SDK allows access to much lower level things when developing a classic app. For this reason, the Android NDK offers an API to call Java code from C++ code through JNI (Java Native Interface).

For example, in my case, I had to use this tool when taking care of the French/English translation of the game: you can of course choose the language from the game menu, but when the game is launched for the first time, it's still better if the game uses the language of the system by default. For that, on Linux, I use std::locale("").name() which, on my French computer, returns fr_FR (for “French of France”). An American computer will say en_US, etc.

Unfortunately, when compiling for android, std::locale won't return anything, the feature is not available. You can detect the language, but only from the Java interface. This is where NYI comes in! I'll skip the technical details, but we need to retrieve the Java environment and the current activity (application). SDL gives us these accesses via SDL_AndroidGetJNIEnv() and SDL_AndroidGetActivity(). From there, we can use the JNI functions as we want!

In practice, in the Java code, we have the detection of the language:


import java.util.Locale;

// (...)

// In the SDLActivity class
public static String locale = "";
public static String getLocale()
{
  return locale;
}

// In the constructor
String lang = new String(Locale.getDefault().getLanguage());
if (Locale.getDefault().getCountry().length() > 0)
{
  lang = lang + "_" + Locale.getDefault().getCountry();
}
locale = lang;

And in C++, the call to the Java function:


#if defined(SOSAGE_ANDROID)
#  include <jni.h>
#else
#  include <locale>
#endif

std::string get_locale()
{
#if defined(SOSAGE_ANDROID)
  JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
  jobject activity = (jobject)SDL_AndroidGetActivity();
  jclass jni_class(env->GetObjectClass(activity));
  jmethodID method_id = env->GetStaticMethodID(jni_class, "getLocale", "()Ljava/lang/String;");
  jstring jstr = (jstring) env->CallStaticObjectMethod(jni_class, method_id);
  const char *str = env->GetStringUTFChars(jstr, 0);
  std::string out (str);
  env->ReleaseStringUTFChars(jstr, str);
  env->DeleteLocalRef(jstr);
  env->DeleteLocalRef(jni_class);
  return out;
#else
  return std::locale("").name();
#endif
}

And There you go ! We could do the same for any method of any class we are interested in.

Conclusion

In practice, porting an SDL2 game to Android is not done with just a snap of our fingers: it requires a lot of adaptations, whether in terms of compilation, events, data management and for some system calls.

But the SDL is doing a lot of work for us and offers a very handy toolkit. Then, the work we're doing, we mainly have to do it once: once we have set up these different mechanisms, it works, it does not move, and we can quietly develop our game in C ++ without worrying of this mess.

In future articles, I will no doubt talk to you about porting to Emscripten (which allows us to play the game in a browser) or cross-compilation to Windows from Linux. See you!

i