D8S EURL blog

← Back to Index

SDL sur Android

> SDL2 offre un socle solide et vraiment multi-plateforme pour développer des jeux, principalement, mais aussi des applications. Il est une bonne alternative à des solutions payantes. Mais son usage sur Android n'est pas si simple. Voyons comment l'architecture se décompose avec une application simple.

Architecture de SDL sur Android

La librairie SDL est développée en C et offre une API en C. Dès lors, il faudra utiliser le NDK (Native Developement Kit) fourni par Google qui permet de compiler du code C/C++ pour android et le lier à du code Java.

Je parle de lier, car toute application Android doit avoir au minimum une classe Java principale, l'Activity.

SDL2 ne s'occupe pas que de l'affichage mais aussi des entrées/sorties, de l'audio et des particularités mobiles (par exemple la mise en sommeil et l'orientation de l'écran, l'appui sur les boutons du téléphone...). Tout ceci doit être redirigé vers l'application C++. Dès lors, les développeurs ont développé un set de classes Java qui permettent de faire de pont entre Android et SDL2.

image

Notre application minimale sera donc composée de :

Eh oui, il nous faudra compiler SDL à partir de ses sources car aucune librairie pré-compilée n'existe en standard. Notez que les autres librairies utilitaires optionnelles de SDL peuvent aussi être ajoutées en partant des sources (SDL_image, SDL_ttf, SDL_net ...).

Outillage

Pour développer sur Android, vous aurez besoin :

Nous n'allons pas utiliser forcément Android Studio, la ligne de commande suffira. Et c'est tant mieux, l'IDE de Google est ultra lent et horrible à utiliser.

Le projet utilisera Gradle, le standard sous Android. Il serait possible de construire un paquet APK autrement, à l'aide d'un ensemble de scripts et d'appel à différents outils de Google (d8, appt2, ...) mais c'est déconseillé. Maintenir un tel ensemble de script prend du temps et est risqué : Google fait évoluer régulièrement ses outils et les règles de disffusion sur le PlayStore. Donc, restons sur Gradle.

image

Au niveau logiciel, tout est normalement fourni dans le NDK. Nous utiliserons CMake pour construire la partie C/C++ de notre application, CMake et le compilateur croisé Clang sont dans les outils du NDK.

Arborescence

Les fichiers Gradle de base seront à la racine du dépôt. Le répertoire `app` contiendra tous les fichiers propres à la cible android, dont des sous fichiers Gradle et du code Java (dans `app/src/main/java`). Les fichiers sorties, dont l'APK final, sera généré dans `app/build`.


.
├── app
│   ├── build
│   │   ├── generated
│   │   ├── intermediates
│   │   ├── outputs
│   │   │   ├── apk
│   │   │   └── logs
│   │   └── tmp
│   └── src
│       └── main
│           ├── assets
│           ├── java
│           └── res
├── gradle
│   └── wrapper
├── libs
│   ├── lib_sdl2
│   │   ├── include
│   │   └── src
└── src

Par défaut, le plugin Gradle compilera tous les fichiers Java localisés dans le répertoire `src/main/java` de votre projet (ou sous-projet).

Si vous désirez placer les fichiers dans un autre répertoire il faudra le préciser dans le fichier gradle :


sourceSets {
    main {
        java {
            srcDirs = ['src/java']
        }
        resources {
            srcDirs = ['src/resources']
        }
    }
}

Le répertoire `libs` contiendra la librairie SDL2 sous forme de code source.

Enfin, le répertoire `src` à la racine contiendra le code C/C++ propre à notre application.

Fichier projet Gradle

Les fichiers de base du projet Gradle sont classiques :


build.gradle
gradle.properties 
gradlew 
gradlew.bat
settings.gradle

Le fichier `settings.gradle` contiendra l'inclusion du sous projet Gradle via une inclusion : `include ':app'`.

Les fichiers `gradlew` et `gradlew.bat` sont les scripts bash de lancement (ils sont générés par Gradle en exécutant un `gradle init` ou récupérés d'une autre application). Notez que vous trouverez dans le code source de la librairie SDL2 un exemple de projet Android dans le dossier `android-project`.

Le fichier Gradle de base est classique pour un projet Android et complètement générique. On passe, rien à signaler ici.


buildscript {
    repositories {
        jcenter()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
        google()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"
    }
}

allprojects {
    repositories {
        jcenter()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Dans le répertoire 'app', à la racine, nous retrouvons le fichier Gradle principale pour notre application.


apply plugin: 'com.android.application'

android {
    compileSdkVersion 30

    signingConfigs {
        demokey {
            storeFile file('demokey.jks')
            storePassword "demokey"
            keyAlias 'demokey'
            keyPassword 'demokey'
        }
    }
    defaultConfig {
        applicationId "eu.d8s.galaxie"
        minSdkVersion 18
        targetSdkVersion 30
        versionCode 3
        versionName "2.0.16"
        externalNativeBuild {
            cmake {
                arguments "-DANDROID_NATIVE_API_LEVEL=30", "-DANDROID_STL=c++_shared", "-DANDROID=true", "-DANDROID_TOOLCHAIN=clang"
                cppFlags "-std=c++11 -frtti -fexceptions"
                abiFilters "arm64-v8a"
            }
        }
    }
    buildTypes {
        debug {
            minifyEnabled false
            signingConfig signingConfigs.demokey
        }
        release {
            minifyEnabled true
            signingConfig signingConfigs.demokey
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    applicationVariants.all { variant ->
        variant.outputs.all {
            def fileName = project.name + '-' + variant.name + '-V' +
                    defaultConfig.versionCode + "-" + buildTime() + ".apk"
            outputFileName = fileName
        }
    }
    externalNativeBuild {
        cmake {
            path '../src/CMakeLists.txt'
        }
    }
}

static def buildTime() {
    return new Date().format("yyyyMMdd", TimeZone.getDefault())
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

Voici les parties les plus importantes :

Processus de construction

Le processus est représenté par l'illustration suivante :

image

Le plugin Java de Gradle sait comment compiler les fichiers Java situés dans le répertoire app/src/main, organisés à la Java selon une URL inversée.

Par contre, on ne peut spécifier qu'un seul fichier CMake pour construire le code C/C++. Il faut donc avoir un CMakeLists.txt chapeau qui appellera les sous CMakeLists.txt sous la forme d'appels à `add_subdirectories`.

Fichier CMake principal

Notre fichier principal SDL est très simple il affiche un carré rouge. Nous avons donc qu'un seul fichier C : main.c.

Nous construisons que des librairies dynamiques (.so sous Linux)


# ===========================================================================
# CMAKE PRINCIPAL APPELÉ PAR GRADLE
# ===========================================================================

cmake_minimum_required(VERSION 3.4)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

project(main C CXX)

add_subdirectory(../libs/lib_sdl2 SDL2)

set(MAIN_SRCS
    main.c
)

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

add_library(main ${MAIN_SRCS})

target_include_directories(main PUBLIC ../libs/lib_sdl2/SDL/include)

target_link_libraries(main PUBLIC dl GLESv1_CM GLESv2 OpenSLES log android SDL2)

target_compile_options(main PUBLIC -Wall -Wextra -Wdocumentation -Wdocumentation-unknown-command -Wmissing-prototypes -Wunreachable-code-break -Wunneeded-internal-declaration -Wmissing-variable-declarations 
 -Wfloat-conversion -Wshorten-64-to-32 -Wunreachable-code-return -Wshift-sign-overflow -Wstrict-prototypes -Wkeyword-macro -Wno-unused-parameter -Wno-sign-compare)

Tous nos projets CMake génèreront des librairies dynamiques. Ici, notre CMakeLists.txt inclut un sous projet, la librairie SDL, via l'appel de `add_subdirectory`. Les autres options sont classiques : des options au compilateurs et une liste de librairies à lier à notre librairie, ici essentiellement les librairies OpenGL mobile et les librairies Android.

Fichiers Java

SDL fournit dans son code source les fichiers Java à ajouter à votre projet :


HIDDeviceBLESteamController.java
HIDDevice.java
HIDDeviceManager.java
HIDDeviceUSB.java
SDLActivity.java
SDLAudioManager.java
SDLControllerManager.java
SDL.java

Le fichier principal, l'Activity Android, est SDLActivity.java. Ce fichier se charge de créer une application à la mode Android, initialiser SDL et notre application et rediriger tous les événements mobiles en SDL.

Le package utilisé est `org.libsdl.app` et on peut voir que beaucoup de méthodes de la classe SDLActivity peuvent être surchargées : comme le dit la documentation SDL sur Android, cette classe est vouée à être héritée et les méthodes éventuellement surchargées pour modifier le comportement par défaut.

image

Cet héritage va vous permettre de créer votre propre nom de package et à customiser le comportement de SDL. Dans notre exemple, on crée le fichier suivant :


package eu.d8s.galaxie;

import org.libsdl.app.SDLActivity; 

/* 
 * A sample wrapper class that just calls SDLActivity 
 */ 
public class GalaxieActivity extends SDLActivity
{
 /**
     * This method is called by SDL before loading the native shared libraries.
     * It can be overridden to provide names of shared libraries to be loaded.
     * The default implementation returns the defaults. It never returns null.
     * An array returned by a new implementation must at least contain "SDL2".
     * Also keep in mind that the order the libraries are loaded may matter.
     * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
     */
     
     // Actually, it *is* overridden because we generate .so files manually
    protected String[] getLibraries() {
        return new String[] {
            "SDL2",
            // "SDL2_image",
            // "SDL2_mixer",
            // "SDL2_net",
            // "SDL2_ttf",
            "main"
        };
    }
}

Notre package se nomme `eu.d8s.galaxie` et le fichier est placé dans le répertoire `app/src/main/java/eu/d8s/galaxie`.

Une des méthodes que vous aurez le plus besoin de surcharger est peut-être la fonction getLibraries() qui permet de lister les librairies dynamiques à charger.

Dans notre cas, nous allons créer deux projets C++, donc deux librairies C++ : `libSDL2.so` et `libmain.so`.

Attention, il faut placer en dernier la librairie contenant votre main(). En effet, voici un extrait du code en charge de récupérer le nom de la librairie à appeler au démarrage :


if (libraries.length > 0) {
    library = "lib" + libraries[libraries.length - 1] + ".so";
} else {
    library = "libmain.so";
}

On voit donc que si la liste est non vide, la dernière librairie déclarée est récupérée.

Processus de démarrage

Continuons notre analyse du fonctionnement du portage de la SDL sur Android. Voici le processus un peu résumé de la phase de démarrage de votre application qu'il faut garder en tête :

image

Notre main() en C est renommé en SDL_main via une macro situé dans le fichier SDL.h :


#elif defined(__ANDROID__)
/* On Android SDL provides a Java class in SDLActivity.java that is the
   main activity entry point.

   See docs/README-android.md for more details on extending that class.
 */
#define SDL_MAIN_NEEDED

#if defined(SDL_MAIN_NEEDED) || defined(SDL_MAIN_AVAILABLE)
#define main    SDL_main
#endif

Un démarrage réussi va faire apparaître la ligne suivante sous `adb logcat` :


03-28 15:47:34.252  9746  9767 V SDL     : Running main function SDL_main from library /data/app/eu.d8s.galaxie-t17c1gNT9QZvTh-BahM4Ug==/lib/arm64/libmain.so
03-28 15:47:34.252  9746  9767 V SDL     : nativeRunMain()

Ici la fonction SDL_main a bien été trouvée dans la librairie `libmain.so`.

Fichier AndroidManifest.xml

Ici nous retrouvons un fichier de base assez classique. Les lignes à changer pour votre application sont :

Le fichier est extrait du code source de SDL et inclut quelques conseils.





    
    

    
    

    
    

    
    

    
    
    
    

    
    

    
    

        

        
            
                
                
            
            
            
        
    


La compilation de SDL

Alors c'est un peu le bazar.

D'une part, SDL2 est une "vieille" librairie mine de rien et son système de build est en cours d'évolution :

Autre difficulté : toute la configuration de la SDL est centralisée dans un fichier unique : SDL_config.h. Des squelettes prêts à l'emploi sont disponibles dans le même répertoire (include) : SDL_config_android.h, SDL_config_emscripten.h, SDL_config_iphoneos.h ... Il faut donc remplacer le fichier SDL_config.h par la bonne version selon votre cible.

Cela pose un soucis au niveau du code source : il n'est pas très propre de modifier une librairie tierce, cela pose des soucis de maintenance. Sauf si cette librairie tierce n'est pas mise sur Git, que ce soit sous la forme de sous module ou directement sous forme de code source.

Nous allons utiliser une fonctionnalité de CMake qui permet de récupérer le code, soit à partir d'une archive, soit à partir d'un dépôt Git, et nous allons le patcher :


include(FetchContent)

FetchContent_Declare(
    sdl2_project
    URL      https://www.libsdl.org/release/SDL2-2.0.20.zip
)

FetchContent_GetProperties(sdl2_project)
if(NOT sdl2_project_POPULATED)
  FetchContent_Populate(sdl2_project)

  # Copy an additional/replacement file into the populated source
  # COPY_FILE is available from CMake 3.21 only
  #file(COPY_FILE ${sdl2_project_SOURCE_DIR}/include/SDL_config_android.h ${sdl2_project_SOURCE_DIR}/include/SDL_config.h)
  execute_process(COMMAND cp ${sdl2_project_SOURCE_DIR}/include/SDL_config_android.h ${sdl2_project_SOURCE_DIR}/include/SDL_config.h)
  message("Overwrite SDL_config.h with SDL_config_android.h")

endif()

FetchContent_MakeAvailable(sdl2_project)
add_subdirectory(${sdl2_project_SOURCE_DIR} SDL2)

set(SDL2_HEADERS
    ${sdl2_project_SOURCE_DIR}/include
)

Le code est vraiment simple et facile à maintenir :

Bingo, la SDL se construit parfaitement et la librairie libSDL2.so est bien générée.

Application SDL

Terminons par notre application proprement dite située dans le fichier main.c : nous allons afficher un joli carré rouge. La boucle d'événements est classique pour un programme SDL :


#include 
#include 

#include 

int main(int argc, char *argv[]) {
    SDL_Window *window;
    SDL_Renderer *renderer;

    if (SDL_CreateWindowAndRenderer(0, 0, 0, &window, &renderer) < 0)
        exit(2);
   
    /* Main render loop */
    Uint8 done = 0;
    SDL_Event event;
    while (!done) {
        /* Check for events */
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT || event.type == SDL_KEYDOWN ||
                event.type == SDL_FINGERDOWN) {
                done = 1;
            }
        }
        /* Draw a gray background */
        SDL_SetRenderDrawColor(renderer, 0xA0, 0xA0, 0xA0, 0xFF);
        SDL_RenderClear(renderer);
        
        //Render red filled quad
        SDL_Rect fillRect = { 20, 20, 100, 300 };// SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 };
        SDL_SetRenderDrawColor( renderer, 0xFF, 0x00, 0x00, 0xFF );        
        SDL_RenderFillRect( renderer, &fillRect );

        /* Update the screen! */
        SDL_RenderPresent(renderer);
        SDL_Delay(10);
    }
    exit(0);
}

Et voilà le résultat :

image

Retrouvez le code source de cet article (et plus) ici : https://github.com/arabine/galaxie-de-mots