Android+SDL: Будь мужиком, пиши на C!

Я люблю C.
Я любою джаву не меньше, но это совсем не то чувство первой любви, которое я испытываю к C.

Я никогда не работал с SDL, но мне кажется, что он очень удобен для написания игр. По крайней мере я бы купился на его кросс-платформенность. Тем более, что теперь в списке его платформ еще и Android.

Установка

Я использовал версию из репозитория (SDL 2.0):

$ hg clone http://hg.libsdl.org/SDL

Далее необходимо несколько изменить структуру папок, чтобы получился Android-проект.

1) Копируем папку SDL/android-project в, скажем, sdl-demo (это наша папка с SDL проектом)
2) Копируем саму папку SDL в sdl-demo/jni (получаем sdl-demo/jni/SDL)
3) Редактируем jni/src/Android.mk, заменив YourSourceHere.c на список файлов вашего проекта (для простоты — только main.c)
4) Создадим этот самый main.c (путь к нему: sdl-demo/jni/src/main.c).
5) Теперь native-часть нашего проекта должна собираться.

Вот как это было у меня:

$ cp -r SDL/android-project sdl-demo
$ cp -r SDL sdl-demo/jni
$ sed -i 's/YourSourceHere.c/main.c/' sdl-demo/jni/src/Android.mk
$ cat > sdl-demo/jni/src/main.c << EOF
int SDL_main(int argc, char *argv[]) {
  return 0;
}
EOF
$ cd sdl-demo/jni
$ ndk-build
Compile thumb  : SDL2 <= SDL_error.c
Compile thumb  : SDL2 <= SDL_assert.c
Compile thumb  : SDL2 <= SDL_log.c
Compile thumb  : SDL2 <= SDL.c
....
Compile thumb  : SDL2 <= SDL_androidkeyboard.c
StaticLibrary  : libstdc++.a
SharedLibrary  : libSDL2.so
Install        : libSDL2.so => libs/armeabi/libSDL2.so
Compile++ thumb  : main <= SDL_android_main.cpp
Compile thumb  : main <= main.c
SharedLibrary  : libmain.so
Install        : libmain.so => libs/armeabi/libmain.so

Доработать напильником

И хотя libSDL готова, этого еще недостаточно чтобы заставить ее работать под Android. Дело в том, что SDL для Android — это кружево из native-функций, и все они вызываются из Java-класса SDLActivity (см. sdl-demo/src/org/libsdl/app/SDLActivity.java).

Так вот, общая схема работы такая: запустить отдельный поток, а в нем SDL_main().
А после выхода из SDL_main().. не делать ничего. Хотя, логично было бы завершить Activity.

Это можно сделать руками. В просто случае это runOnUiThread()+finish(). Но я сделал через статические методы, по аналогии с COMMAND_CHANGE_TITLE:

static int COMMAND_CHANGE_TITLE = 1;
static int COMMAND_QUIT = 2; // <-- НАША КОМАНДА: ЗАВЕРШИТЬ ACTIVITY
...
Handler commandHandler = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.arg1 == COMMAND_CHANGE_TITLE) {
                setTitle((String)msg.obj);
            } else if (msg.arg1 == COMMAND_QUIT) { // <-- ДОБАВИМ НАШУ КОМАНДУ
		finish(); 
            }
        }
};
...
public static void finishActivity() {
        mSingleton.sendCommand(COMMAND_QUIT, null); // <-- ОБЕРТКА ДЛЯ ВЫЗОВА COMMAND_QUIT
}
...
class SDLMain implements Runnable {
    public void run() {
        // Runs SDL_main()
        SDLActivity.nativeInit();
        SDLActivity.finishActivity(); // <-- ДОБАВИМ ВЫЗОВ finishActivity
        //Log.v("SDL", "SDL thread terminated");
    }
}
...

Понимаете о чем я?

Теперь можно обновить проект, если надо, и собрать его целиком:

$ android update project <ваши опции, напр. -t 1 -p .>
$ ant debug

Пример

Теперь вы можете писать приложения на SDL в main.c. Вот что получилось у меня:

#include "SDL.h"
#include <android/log.h>

#define LOG(...) __android_log_print(ANDROID_LOG_DEBUG, "sdldemo", __VA_ARGS__)

int SDL_main(int argc, char *argv[]) {
	SDL_Window *window = NULL;
	SDL_Renderer *renderer = NULL;
	SDL_Texture *bitmapTex = NULL;
	SDL_Surface *bitmapSurface = NULL;

	int running = 1;

	LOG("started");

	SDL_Init(SDL_INIT_VIDEO);

	window = SDL_CreateWindow("SDL Hello World",
			SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
			640, 480, SDL_WINDOW_SHOWN);

	renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

	bitmapSurface = SDL_LoadBMP("data/test.bmp");
	bitmapTex = SDL_CreateTextureFromSurface(renderer, bitmapSurface);
	SDL_FreeSurface(bitmapSurface);

	while (running) {
		SDL_Event event;

		SDL_RenderClear(renderer);
		SDL_RenderCopy(renderer, bitmapTex, NULL, NULL);
		SDL_RenderPresent(renderer);

		while (SDL_PollEvent(&event)) {
			if (event.type == SDL_KEYDOWN && 
				event.key.keysym.scancode == SDL_SCANCODE_AC_BACK) {
				LOG("pressed back button, terminating");
				running = 0;
			}
		}
	}

	LOG("finished");

	SDL_DestroyTexture(bitmapTex);
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_Quit();
	return 0;
}

Данный пример я по кускам спёр в разных уголках интернета, но в целом он понятен: создали окно (640х480 — это логические размеры, физические берутся из onSurfaceChanged()). Потом делаем рендерер, и загружаем текстуру. Текстура загружается из «assets/data/test.bmp».

Затем в цикле рисуем текстуру с помощью рендерера, и ждем следующего события. Особым образом обрабатываем нажание на кнопку «Back» — завершаем наш цикл. А потом чистим все ресурсы.

Итоги

Собираем, запускаем. На телефоне выглядит примерно так:

Android SDL example

SDL для Android: наш hello world

Теперь можно учить SDL и писать игры. Хотя путь еще долгий — хочется SDL_ttf, SDL_image, чтобы работать с комфортом. Но это уже совсем другая история.

Кстати, overhead от использования SDL ни много ни мало — 200-300 Кб.

Все исходники (около 6 мегабайт, SDL много весит, и так удалил тесты и папку контроля версий): https://rapidshare.com/files/4157028483/sdl-android.tar.gz
Лучше, пожалуй, какой-то скрипт для развертывания SDL-проекта написать.

UPD: Ссылка на rapidshare безнадежно устарела. Сделал заново по инструкциям для свежего SDL: https://dl.dropboxusercontent.com/u/5689099/sdl-demo.tar.gz

16 comments on “Android+SDL: Будь мужиком, пиши на C!

  1. Спасибо за подробный разбор.

  2. Pavel:

    спасибо, а проект можешь целиком выложить?

  3. Pavel:

    ты не в курсе, как быть со старыми функциями SDL_SetVideoMode, SDL_DisplayFormat, SDL_Flip, в новой версии их нет, есть какие-то замены или как?

  4. Бодрого!

    А ты в курсе как грузится линукс(Android) в SMP multicore? Т.е. с чего он стартует (0-е ядро?) и в каком месте и как начинает использовать остальные ядра?

  5. Loki:

    скачал Ваш проект, ничего не менял, с помощью eclipse создал проект с готовых исходников, эмулятор андроид 4.03, запустил. в итоге на месте, где надо грузить изображение — крешится( не подскажете, в чем может быть дело?

    • Вынужден подтвердить. На самом деле все довольно просто:
      1) Вы могли поставить эмулятор для x86 (который сборка андроида от Intel, в составе SDK, щас многие ее любят ставить, т.к. быстрее работает). Тогда нужно добавить jni/Application.mk с одной строчкой — APP_API := armeabi x86
      2) Даже если у Вас ARM сборка, или Вы пересобрали SDL для x86 — все равно огромные шансы не заработать. Будет крэш во время SDL_CreateRenderer(), потому что там указано — только accelerated renderer (а эмуляторы по умолчанию, а некоторые и вообще не умеют делать аппаратное ускорение). Поэтому — либо придется тестировать на девайсе, либо шаманить с эмулятором, чтобы в нем заработал аппаратный OpenGL.
      3) либо я не угадал, и дело в чем-то другом 🙂

      • Loki:

        Подумал, что таки да — графическое ускорение выключено. Нашел, как включить — запускаю — та же беда. Модифицировал код (то есть как модифицировал — в ключевых местах поставил запись в лог
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
        LOG(«!!!created renderer»);
        bitmapSurface = SDL_LoadBMP(«data/test.bmp»);
        if(bitmapSurface != NULL)
        {
        LOG(«!!!load image»);
        bitmapTex = SDL_CreateTextureFromSurface(renderer, bitmapSurface);
        SDL_FreeSurface(bitmapSurface);
        }
        else
        {
        LOG(«!!!failed load image»);
        }
        и судя по логах — оно создает renderrer…
        05-29 09:42:48.666: D/sdldemo(592): !!!created renderer
        05-29 09:42:48.676: E/dalvikvm(592): JNI ERROR (app bug): accessed stale local reference 0x1d200029 (index 10 in a table of size 3)

        Я совсем не знаком с android (так случилось, что надо протестировать sdl для него), и я не понимаю, как (КАК?!?) картинка, которая находится в папке “assets/data/test.bmp”, но по отношению к коду main.c совсем не там — может подгружаться правильно? Это магия андроида?)

      • Странного ничего нет, во-первых путь к картинке относительный, а значит если бы current working directory была папка ./assets/, то все заработало бы как надо. Магия в том, что в андроиде ресурсы, которые лежат в assets, открываются как обычные файлы с относительным путем. По крайней мере в джаве если обращаться к файлу по пути «dir1/dir2/file.txt», от это будет соответствовать пути assets/dir1/dir2/file.txt в структуре проекта.

        Наверное потому что андроид приложение — это zip-архив который на лету распаковывается и выполняется, то получается, что вся работа с файлами-ресурсами в составе приложений происходит особым образом.

        Это можно увидеть в сорцах SDL: В SDL_android.c есть функиця Android_JNI_FileOpen, которая открывает файл не через fopen/open, а через java-классы типа AssetManager. Так вот SDL_rwops.c (в котором функции работы с файлами в SDL) использует как раз Android_JNI_FileOPen. Так что не удивляйтесь, все относительные пути начинаются от папки assets/.

      • Loki:

        ммм… пробовал запустить на реальном устройстве — тоже ничего не получилось. и кажется проблема как раз в загрузке изображения
        LOG(«!!!created renderer»);
        bitmapSurface = SDL_LoadBMP(«data/test.bmp»);
        LOG(«!!!urrraa»);
        до «ура» оно не доходит. ничего не понимаю…

      • Loki:

        Если интересно, то это проблема как рас 4го андроида. http://bugzilla.libsdl.org/show_bug.cgi?id=1500

  6. Arc:

    Serg! Очень интересный блог и этот пост в частности! Не могу взять исходники с рапидшары — ссылка не работает… а хочется посмотреть(

  7. ERET1K:

    ДА вот это настоящая кросплатформенность! Скопировал тупо код пример и хоть это мелочь но все работает! http://s017.radikal.ru/i427/1401/fe/894b437fc8fe.png

    Спасибо, действительно нормальный пример кода.

Ответить на Loki Отменить ответ