Blog
Porting an SDL game on Android (2/2)
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:
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 typeSDL_MOUSEBUTTONDOWN
with the valuesev.button.x == 960
andev.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 typeSDL_FINGERDOWN
with the valuesev.button.x == 0.5
andev.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 valueANDROID_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 haveSDL_APPDIDENTERBACKGROUND
which is generated when the application is effectively put in the background (the difference is subtle);- similarly, we have
SDL_APP_WILLENTERFOREGROUND
andSDL_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 theSDL_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!