diff --git a/examples/cross_build/README.md b/examples/cross_build/README.md index d0dd7f55..a2874315 100644 --- a/examples/cross_build/README.md +++ b/examples/cross_build/README.md @@ -8,3 +8,8 @@ ### [Use Android NDK to cross-build](android/ndk_basic) - Learn how to cross-build packages for Android. [Docs](https://docs.conan.io/2/examples/cross_build/android.html) + + +### [GameDev Raylib: Running on Android](android/raylib) + +- Learn how to port your Raylib C++ game to Android using Android Studio, the NDK, and Conan for dependency management. [Blog](https://blog.conan.io/cpp/gamedev/android/conan/raylib/2025/11/24/GameDev-Raylib-Android.html) diff --git a/examples/cross_build/android/raylib/app/build.gradle b/examples/cross_build/android/raylib/app/build.gradle new file mode 100644 index 00000000..618ad0ec --- /dev/null +++ b/examples/cross_build/android/raylib/app/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'com.android.application' +} + +task conanInstall { + def conanExecutable = "conan" // define the path to your conan installation + def buildDir = new File("app/build") + buildDir.mkdirs() + ["Debug", "Release"].each { String build_type -> + ["armv8"].each { String arch -> + def cmd = conanExecutable + " install " + + "../src/main/cpp --profile android -s build_type="+ build_type +" -s arch=" + arch + + " --build missing -c tools.cmake.cmake_layout:build_folder_vars=['settings.arch']" + print(">> ${cmd} \n") + + def sout = new StringBuilder(), serr = new StringBuilder() + def proc = cmd.execute(null, buildDir) + proc.consumeProcessOutput(sout, serr) + proc.waitFor() + println "$sout $serr" + if (proc.exitValue() != 0) { + throw new Exception("out> $sout err> $serr" + "\nCommand: ${cmd}") + } + } + } +} + +android { + namespace 'com.example.raylibexample' + compileSdk 34 + + defaultConfig { + applicationId "com.example.raylibexample" + minSdk 27 + targetSdk 34 + versionCode 1 + versionName "1.0" + + ndk { + abiFilters 'arm64-v8a' + } + + externalNativeBuild { + cmake { + cppFlags '-v' + arguments("-DCMAKE_TOOLCHAIN_FILE=conan_android_toolchain.cmake", "-DANDROID_STL=c++_shared") + } + } + } + + buildTypes { + release { + minifyEnabled false + } + } +} \ No newline at end of file diff --git a/examples/cross_build/android/raylib/app/src/main/AndroidManifest.xml b/examples/cross_build/android/raylib/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8f58e318 --- /dev/null +++ b/examples/cross_build/android/raylib/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/cross_build/android/raylib/app/src/main/cpp/CMakeLists.txt b/examples/cross_build/android/raylib/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..515b501a --- /dev/null +++ b/examples/cross_build/android/raylib/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.22.1) +project("raylibexample" LANGUAGES C CXX) + +set(NATIVE_APP_GLUE_DIR ${ANDROID_NDK}/sources/android/native_app_glue) + +find_package(raylib CONFIG REQUIRED) + +add_library(${CMAKE_PROJECT_NAME} SHARED) + +target_sources(${CMAKE_PROJECT_NAME} PRIVATE + ${NATIVE_APP_GLUE_DIR}/android_native_app_glue.c + native-lib.cpp) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE + ${NATIVE_APP_GLUE_DIR}) + +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE + android + log + EGL + GLESv2 + OpenSLES + m + raylib) \ No newline at end of file diff --git a/examples/cross_build/android/raylib/app/src/main/cpp/conan_android_toolchain.cmake b/examples/cross_build/android/raylib/app/src/main/cpp/conan_android_toolchain.cmake new file mode 100644 index 00000000..8ea03e74 --- /dev/null +++ b/examples/cross_build/android/raylib/app/src/main/cpp/conan_android_toolchain.cmake @@ -0,0 +1,15 @@ +if ( NOT ANDROID_ABI OR NOT CMAKE_BUILD_TYPE ) + return() +endif() + +if(${ANDROID_ABI} STREQUAL "x86_64") + include("${CMAKE_CURRENT_LIST_DIR}/build/x86_64/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake") +elseif(${ANDROID_ABI} STREQUAL "x86") + include("${CMAKE_CURRENT_LIST_DIR}/build/x86/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake") +elseif(${ANDROID_ABI} STREQUAL "arm64-v8a") + include("${CMAKE_CURRENT_LIST_DIR}/build/armv8/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake") +elseif(${ANDROID_ABI} STREQUAL "armeabi-v7a") + include("${CMAKE_CURRENT_LIST_DIR}/build/armv7/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake") +else() + message(FATAL_ERROR "Not supported configuration: ${ANDROID_ABI}") +endif() \ No newline at end of file diff --git a/examples/cross_build/android/raylib/app/src/main/cpp/conanfile.txt b/examples/cross_build/android/raylib/app/src/main/cpp/conanfile.txt new file mode 100644 index 00000000..b556b149 --- /dev/null +++ b/examples/cross_build/android/raylib/app/src/main/cpp/conanfile.txt @@ -0,0 +1,9 @@ +[requires] +raylib/5.5 + +[generators] +CMakeToolchain +CMakeDeps + +[layout] +cmake_layout diff --git a/examples/cross_build/android/raylib/app/src/main/cpp/native-lib.cpp b/examples/cross_build/android/raylib/app/src/main/cpp/native-lib.cpp new file mode 100644 index 00000000..76f53fa5 --- /dev/null +++ b/examples/cross_build/android/raylib/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include "raylib.h" +#include + +#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "RaylibApp", __VA_ARGS__)) + +extern "C" { + +int main() { + LOGI("Starting Raylib"); + // --- Initialization --- + const int screenW = 800; + const int screenH = 450; + InitWindow(screenW, screenH, "Jump to Survive!"); + SetExitKey(0); // Disable back button from closing app automatically + + // --- Player Setup --- + Rectangle player = { 100, screenH - 80, 40, 60 }; + float vy = 0; + const float gravity = 1000.0f; + const float jumpImpulse = -450.0f; + + // --- Ground Definition --- + const int groundY = screenH - 20; + + // --- Obstacle Management --- + std::vector obstacles; + float spawnTimer = 0.0f; + float spawnInterval = 1.2f; + const float obstacleSpeed = 300.0f; + + const float minSpawnInterval = 0.8f; + const float maxSpawnInterval = 1.6f; + + const int minObsWidth = 40; + const int maxObsWidth = 120; + + // --- Game State Variables --- + int score = 0; + bool gameOver = false; + float gameOverTimer = 0.0f; + + // --- Back button double press logic --- + float backPressTime = 0.0f; + bool backPressedOnce = false; + const float doublePressInterval = 0.5f; // 0.5 seconds + + SetTargetFPS(60); + + while (!WindowShouldClose()) { + float dt = GetFrameTime(); + + // --- Back button exit logic --- + if (backPressedOnce) { + backPressTime += dt; + if (backPressTime > doublePressInterval) { + backPressedOnce = false; + } + } + + if (IsKeyPressed(KEY_BACK)) { + if (backPressedOnce) { + break; // Exit game + } else { + backPressedOnce = true; + backPressTime = 0.0f; + } + } + + + if (!gameOver) { + // Jump logic + if (GetTouchPointCount() > 0 && player.y + player.height >= groundY) { + vy = jumpImpulse; + } + vy += gravity * dt; + player.y += vy * dt; + if (player.y + player.height > groundY) { + player.y = groundY - player.height; + vy = 0; + } + + // Spawn obstacles with random width & interval + spawnTimer += dt; + if (spawnTimer >= spawnInterval) { + spawnTimer = 0.0f; + // recalc next interval + spawnInterval = GetRandomValue(int(minSpawnInterval*100), int(maxSpawnInterval*100)) / 100.0f; + // random width + int w = GetRandomValue(minObsWidth, maxObsWidth); + obstacles.push_back({ float(screenW), float(groundY - 40), float(w), 40.0f }); + } + + // Move & collide obstacles + for (int i = 0; i < (int)obstacles.size(); i++) { + obstacles[i].x -= obstacleSpeed * dt; + if (CheckCollisionRecs(player, obstacles[i])) { + gameOver = true; + gameOverTimer = 0.0f; // Reset timer on game over + } + } + // Remove off-screen & score + if (!obstacles.empty() && obstacles.front().x + obstacles.front().width < 0) { + obstacles.erase(obstacles.begin()); + score++; + } + } + else { + gameOverTimer += dt; + // Want to wait 2 seconds before accepting the restart + if (GetTouchPointCount() > 0 && gameOverTimer > 1.0f) { + // reset everything + player.y = screenH - 80; + vy = 0; + obstacles.clear(); + spawnTimer = 0.0f; + spawnInterval = 1.2f; + score = 0; + gameOver = false; + } + } + + // --- Drawing --- + BeginDrawing(); + ClearBackground(RAYWHITE); + + DrawRectangle(0, groundY, screenW, 20, DARKGRAY); + DrawRectangleRec(player, BLUE); + for (auto &obs : obstacles) DrawRectangleRec(obs, RED); + + DrawText(TextFormat("Score: %d", score), 10, 10, 20, BLACK); + + if (gameOver) { + DrawText("GAME OVER! Tap to restart", 200, screenH/2 - 20, 20, MAROON); + } + + if (backPressedOnce) { + const char *msg = "Press back again to exit"; + int textWidth = MeasureText(msg, 20); + DrawText(msg, (screenW - textWidth) / 2, screenH - 420, 20, BLACK); + } + + EndDrawing(); + } + + CloseWindow(); + return 0; +} + +} // extern "C" diff --git a/examples/cross_build/android/raylib/build.gradle b/examples/cross_build/android/raylib/build.gradle new file mode 100644 index 00000000..36941ff5 --- /dev/null +++ b/examples/cross_build/android/raylib/build.gradle @@ -0,0 +1,9 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.2.0' + } +} diff --git a/examples/cross_build/android/raylib/ci_test_example.py b/examples/cross_build/android/raylib/ci_test_example.py new file mode 100644 index 00000000..a3525dfd --- /dev/null +++ b/examples/cross_build/android/raylib/ci_test_example.py @@ -0,0 +1,33 @@ +import os + +from test.examples_tools import run + +# ############# Example ################ +print("- Use the Android NDK to cross-build a package -") + + +profile = """ + +[settings] +os=Android +os.api_level=27 +arch=armv8 +compiler=clang +compiler.version=18 +compiler.libcxx=c++_static +compiler.cppstd=17 +build_type=Debug + +[conf] +tools.android:ndk_path={} +""" + +ndk_path = os.environ.get("ANDROID_NDK") or os.environ.get("ANDROID_NDK_HOME") +if ndk_path: + profile = profile.format(ndk_path) + os.makedirs(os.path.join("app", "build"), exist_ok=True) + with open(os.path.join("app", "build", "android"), "w") as fd: + fd.write(profile) + + run("gradle --no-daemon assembleDebug") + assert os.path.exists(os.path.join("app", "build", "outputs", "apk", "debug", "app-debug.apk")) diff --git a/examples/cross_build/android/raylib/settings.gradle b/examples/cross_build/android/raylib/settings.gradle new file mode 100644 index 00000000..28ab1b0e --- /dev/null +++ b/examples/cross_build/android/raylib/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = 'RaylibExample' +include ':app' \ No newline at end of file