Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/cross_build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
56 changes: 56 additions & 0 deletions examples/cross_build/android/raylib/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Jump Game"
android:hasCode="false">
<activity
android:name="android.app.NativeActivity"
android:exported="true"
android:configChanges="orientation|keyboardHidden">
<meta-data
android:name="android.app.lib_name"
android:value="raylibexample" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[requires]
raylib/5.5

[generators]
CMakeToolchain
CMakeDeps

[layout]
cmake_layout
152 changes: 152 additions & 0 deletions examples/cross_build/android/raylib/app/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#include <jni.h>
#include <android/log.h>
#include <android/native_activity.h>
#include "raylib.h"
#include <vector>

#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<Rectangle> 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"
9 changes: 9 additions & 0 deletions examples/cross_build/android/raylib/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0'
}
}
33 changes: 33 additions & 0 deletions examples/cross_build/android/raylib/ci_test_example.py
Original file line number Diff line number Diff line change
@@ -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"))
18 changes: 18 additions & 0 deletions examples/cross_build/android/raylib/settings.gradle
Original file line number Diff line number Diff line change
@@ -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'