Blog
Porter un jeu SDL sur Android (1/2)
Très tôt dans le développement du jeu, j'ai eu dans l'idée de le porter sur Android : le style de jeu (point and click) s'y prête bien, j'ai par exemple beaucoup aimé jouer à Thimbleweed Park sur tablette (et pouvoir jouer aux vieux LucasArts sur son téléphone via ScummVM est un plaisir aussi).
C'est une des raisons qui m'ont vite poussé à passer de la SDL1 à la SDL2 qui, en plus de fournir une accélération matérielle bienvenue pour afficher des images et animations en haute résolution, supporte Android (et fournit même pas mal d'outils pour gérer cette plateforme, on va y venir).
Prérequis
En premier lieu, il faut installer deux choses :
- le SDK Android (Software Development Kit, kit de développement logiciel), qui permet de générer des applications Android à partir de sources Java (et de tout un tas d'autres trucs dont je vais aussi parler) ;
- le NDK Android (Native Development Kit, kit de développement natif), qui permet quant à lui d'intégrer du code dans un autre langage comme C++ dans mon cas.
Je ne vais pas donner de tuto pour ces deux points, vous en trouverez déjà pléthore sur Internet. Notez que Google promeut énormément Android Studio, qui est un IDE spécialisé pour Android. Si comme moi, vous préférez qu'on vous file des scripts et qu'on vous foute la paix sur la façon dont vous voulez programmer, vous pouvez également trouver le SDK tout seul (dans la section Commande line tools only). Pareil pour le NDK.
Une fois qu'on a installé ces deux composants, on peut commencer.
Un point important : en général, quand vous développez un programme sur Linux ou Windows, vous allez d'abord chercher à compiler votre programme. La partie « distribution » (les icones, le programme d'installation, etc.) ne vient qu'après et est indépendante.
Pour Android, ça se passe assez différemment : un fichier APK (que vous récupérez quand vous installer une appli sur Android) n'est pas un simple exécutable (comme un EXE sur Windows) mais bien un logiciel packagé et prêt à être installé en un clic. Du coup, un projet Android va nécessairement comporter pas mal de choses que vous ne verrez pas forcément dans les repos de logiciels sur d'autres plateformes : les icones encore une fois, mais aussi les métadonnées, la description du logiciel, les traductions du titre dans différentes langues, etc.
Première bonne nouvelle : la SDL nous fournit un squelette de projet
déjà tout prêt à être utilisé. Il s'agit du dossier android-project
qui est inclus dans les sources de la
SDL2. Ce squelette contient
notamment « glue » entre votre code et le hardware géré par Android,
qui est un sacré boulot qu'on ne voudrait pas avoir à faire à la main…
En clair : la partie Java est déjà faite, on n'aura quasiment pas à y
toucher, il n'y a plus qu'à y brancher notre code C++, nos données et
tout le reste.
Y'a quoi dans un APK ?
Jetons un œil à l'arborescence de cet 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
En principe, on ne devrait pas avoir besoin de changer les fichiers
Java dans app/src/main/org/libsdl/app
qui contiennent, comme je le
disais, la « glue » entre votre code C++ et le portage SDL sur
Android.
Les fichiers avec gradle
dans le nom sont les fichiers de
configuration du système de build d'Android (Gradlew). En général
ils sont prêts à l'emploi, il suffit de modifier quelques
variables. Par exemple, dans app/build.gradle
, on trouve la ligne
suivante :
applicationId "org.libsdl.app"
Qu'on prendra soin de remplacer par l'identifiant de notre application, soit ici :
applicationId "net.ptilouk.superfluous"
Identifiant de l'application qui va également être le chemin vers le
fichier où va se trouver la classe Java principale de notre
application, et qu'il faut créer. Ici, il s'appellera donc
app/src/main/java/net/ptilouk/superfluous/superfluous.java
et
contiendra juste quelques lignes :
package net.ptilouk.superfluous;
import org.libsdl.app.SDLActivity;
public class superfluous extends SDLActivity { }
De même, on prend soin d'indiquer l'identifiant de l'application et le
nom de l'activité (la classe qui hérite de SDLActivity
) dans le
fichier app/src/main/AndroidManifest.xml
, ainsi que les numéros de
versions qui permettront à Android de détecter si le logiciel doit
être mis à jour :
package="net.ptilouk.superfluous"
android:versionCode="101000"
android:versionName="1.1.0-modern-ui"
<activity android:name="superfluous"
Les images PNG contenues dans les dossiers app/src/main/res/mipmap-*
sont les icones de l'application dans différentes résolutions et qu'on
peut donc remplacer par les nôtres :
Le dossier app/src/main/res/values
contient des chaînes de
caractère, notamment le nom de l'application. Pour gérer les
traductions, on peut ajouter des dossiers similaires en ajoutant le
code de la langue au nom du dossier, par exemple value-fr
pour le
français dans mon cas.
Ainsi, app/src/main/res/values/strings.xml
contient :
<resources>
<string name="app_name">Superfluous Returnz</string>
</resources>
Tandis que app/src/main/res/values-fr/strings.xml
contient :
<resources>
<string name="app_name">Superflu Riteurnz</string>
</resources>
(Je sais, la différence de nom est subtile.)
Compiler du C++ avec bibliothèques tierces
On va passer par les fichiers .mk
(Makefile) pour configurer notre
application C++. Celui qui gère notre application C++ est
app/jni/src/Android.mk
et contient, à l'origine, ça :
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)
Tout d'abord, on veut ajouter nos fichiers sources et nos includes
qui se trouvent dans un dossier stocké dans la variable SRC_PATH
, on
ajoute donc ces informations :
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)
On va aussi ajouter les flags C++ dont on a besoin :
LOCAL_CPPFLAGS := -std=c++17 -frtti -O3 -DNDEBUG \
-DSOSAGE_DATA_FOLDER=\"data/\"
On va donc utiliser C++17, activer les optimisations du compilateur au maximum et de désactiver les informations de débuguage (ça, on peut le retirer pendant qu'on développe, bien sûr).
Le flag -frtti
est un détail technique : par défaut, le NDK
désactive le Run-Time Type Information (RTTI), la détection de type
à l'exécution (par l'intermédiaire de dynamic_cast<>()
en
C++). Comme j'utilise ça assez massivement dans mon code, je le
réactive avec ce flag.
La variable SOSAGE_DATA_FOLDER
est utilisée pour indiquer où se
trouvent les données du jeu. Sur Gnunux, par exemple, ce sera dans
/usr/share/superfluous-returnz/
. Sur Android, la gestion des
assets (les données) est assez particulière, car le programme de
« voit » qu'un dossier virtuel qui va contenir spécifiquement les
données du jeu et qui correspond au dossier app/src/main/assets/
.
Donc ici, on va copier nos données dans app/src/main/assets/data/
et
on indique donc simplement data/
comme dossier.
Au niveau des dépendances, SDL est bien sûr déjà inclus, mais il manque les composants SDL supplémentaires que j'utilise, ainsi que deux autre bibliothèques tierces, Yaml et LZ4. Je les ajoute.
Le fichier complet ressemble donc maintenant à ça :
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)
On prend soin de copier les sources (où de faire un symlink si on
préfère) des différents composants SDL, de Yaml et de LZ4 vers les
dossiers spécifiés. Les différents composants SDL fournissent
directement des fichiers Android.mk
pour la compilation vers
Android, mais ce n'est pas le cas pour Yaml et LZ4. Qu'à cela ne
tienne, il suffit de les écrire ! On peut largement s'inspirer de ce
qui était fait pour SDL.
Pour 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)
Pour LZ4, quasiment la même chose :
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)
Et voilà ! On peut maintenant compiler notre application en une commande, à la racine :
./gradlew assemble
À suivre
On a maintenant une application qui compile, et c'est déjà pas mal… mais maintenant, comment on gère Android côté code ? C'est ce qu'on verra la prochaine fois !