Skip to content

Commit b5a7aca

Browse files
authored
[example] GameDev Raylib: Running on Android (#210)
* Add Android raylib example Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Add conanfile.txt Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Remove extra projects Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Find gradlew Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Use gradle Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Fix android profile path Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Fix android profile path 2 Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Create build folder Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Update Raylib version Signed-off-by: Uilian Ries <uilianr@jfrog.com> * toolchain Signed-off-by: Uilian Ries <uilianr@jfrog.com> * toolchain Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Validate generated .apk Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Add link to blog post Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Stop gradle daemon after building Signed-off-by: Uilian Ries <uilianr@jfrog.com> * No daemon Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Use Jinja to populate ANDROID_NDK Signed-off-by: Uilian Ries <uilianr@jfrog.com> * Add log ANDROID_NDK environment Signed-off-by: Uilian Ries <uilianr@jfrog.com> --------- Signed-off-by: Uilian Ries <uilianr@jfrog.com>
1 parent cf4cf4c commit b5a7aca

File tree

11 files changed

+336
-0
lines changed

11 files changed

+336
-0
lines changed

examples/cross_build/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@
88
### [Use Android NDK to cross-build](android/ndk_basic)
99

1010
- Learn how to cross-build packages for Android. [Docs](https://docs.conan.io/2/examples/cross_build/android.html)
11+
12+
13+
### [GameDev Raylib: Running on Android](android/raylib)
14+
15+
- Learn how to port your Raylib C++ game to Android using Android Studio, the NDK, and Conan for dependency management.
16+
Configure the Android NDK path by adding `ANDROID_NDK` environment variable pointing to your NDK installation.
17+
[Blog](https://blog.conan.io/cpp/gamedev/android/conan/raylib/2025/11/24/GameDev-Raylib-Android.html)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
plugins {
2+
id 'com.android.application'
3+
}
4+
5+
task conanInstall {
6+
def conanExecutable = "conan" // define the path to your conan installation
7+
def buildDir = new File("app/build")
8+
buildDir.mkdirs()
9+
["Debug", "Release"].each { String build_type ->
10+
["armv8"].each { String arch ->
11+
def cmd = conanExecutable + " install " +
12+
"../src/main/cpp --profile android -s build_type="+ build_type +" -s arch=" + arch +
13+
" --build missing -c tools.cmake.cmake_layout:build_folder_vars=['settings.arch']"
14+
print(">> ${cmd} \n")
15+
16+
def sout = new StringBuilder(), serr = new StringBuilder()
17+
def proc = cmd.execute(null, buildDir)
18+
proc.consumeProcessOutput(sout, serr)
19+
proc.waitFor()
20+
println "$sout $serr"
21+
if (proc.exitValue() != 0) {
22+
throw new Exception("out> $sout err> $serr" + "\nCommand: ${cmd}")
23+
}
24+
}
25+
}
26+
}
27+
28+
android {
29+
namespace 'com.example.raylibexample'
30+
compileSdk 34
31+
32+
defaultConfig {
33+
applicationId "com.example.raylibexample"
34+
minSdk 27
35+
targetSdk 34
36+
versionCode 1
37+
versionName "1.0"
38+
39+
ndk {
40+
abiFilters 'arm64-v8a'
41+
}
42+
43+
externalNativeBuild {
44+
cmake {
45+
cppFlags '-v'
46+
arguments("-DCMAKE_TOOLCHAIN_FILE=conan_android_toolchain.cmake", "-DANDROID_STL=c++_shared")
47+
}
48+
}
49+
}
50+
51+
buildTypes {
52+
release {
53+
minifyEnabled false
54+
}
55+
}
56+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[settings]
2+
os=Android
3+
os.api_level=27
4+
arch=armv8
5+
compiler=clang
6+
compiler.version=18
7+
compiler.libcxx=c++_shared
8+
compiler.cppstd=17
9+
build_type=Release
10+
11+
[conf]
12+
tools.android:ndk_path={{ os.getenv("ANDROID_NDK") }}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
<application
4+
android:label="Jump Game"
5+
android:hasCode="false">
6+
<activity
7+
android:name="android.app.NativeActivity"
8+
android:exported="true"
9+
android:configChanges="orientation|keyboardHidden">
10+
<meta-data
11+
android:name="android.app.lib_name"
12+
android:value="raylibexample" />
13+
<intent-filter>
14+
<action android:name="android.intent.action.MAIN" />
15+
<category android:name="android.intent.category.LAUNCHER" />
16+
</intent-filter>
17+
</activity>
18+
</application>
19+
</manifest>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
cmake_minimum_required(VERSION 3.22.1)
2+
project("raylibexample" LANGUAGES C CXX)
3+
4+
set(NATIVE_APP_GLUE_DIR ${ANDROID_NDK}/sources/android/native_app_glue)
5+
6+
find_package(raylib CONFIG REQUIRED)
7+
8+
add_library(${CMAKE_PROJECT_NAME} SHARED)
9+
10+
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
11+
${NATIVE_APP_GLUE_DIR}/android_native_app_glue.c
12+
native-lib.cpp)
13+
14+
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
15+
${NATIVE_APP_GLUE_DIR})
16+
17+
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE
18+
android
19+
log
20+
EGL
21+
GLESv2
22+
OpenSLES
23+
m
24+
raylib)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
if ( NOT ANDROID_ABI OR NOT CMAKE_BUILD_TYPE )
2+
return()
3+
endif()
4+
5+
if(${ANDROID_ABI} STREQUAL "x86_64")
6+
include("${CMAKE_CURRENT_LIST_DIR}/build/x86_64/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
7+
elseif(${ANDROID_ABI} STREQUAL "x86")
8+
include("${CMAKE_CURRENT_LIST_DIR}/build/x86/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
9+
elseif(${ANDROID_ABI} STREQUAL "arm64-v8a")
10+
include("${CMAKE_CURRENT_LIST_DIR}/build/armv8/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
11+
elseif(${ANDROID_ABI} STREQUAL "armeabi-v7a")
12+
include("${CMAKE_CURRENT_LIST_DIR}/build/armv7/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
13+
else()
14+
message(FATAL_ERROR "Not supported configuration: ${ANDROID_ABI}")
15+
endif()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[requires]
2+
raylib/5.5
3+
4+
[generators]
5+
CMakeToolchain
6+
CMakeDeps
7+
8+
[layout]
9+
cmake_layout
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#include <jni.h>
2+
#include <android/log.h>
3+
#include <android/native_activity.h>
4+
#include "raylib.h"
5+
#include <vector>
6+
7+
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "RaylibApp", __VA_ARGS__))
8+
9+
extern "C" {
10+
11+
int main() {
12+
LOGI("Starting Raylib");
13+
// --- Initialization ---
14+
const int screenW = 800;
15+
const int screenH = 450;
16+
InitWindow(screenW, screenH, "Jump to Survive!");
17+
SetExitKey(0); // Disable back button from closing app automatically
18+
19+
// --- Player Setup ---
20+
Rectangle player = { 100, screenH - 80, 40, 60 };
21+
float vy = 0;
22+
const float gravity = 1000.0f;
23+
const float jumpImpulse = -450.0f;
24+
25+
// --- Ground Definition ---
26+
const int groundY = screenH - 20;
27+
28+
// --- Obstacle Management ---
29+
std::vector<Rectangle> obstacles;
30+
float spawnTimer = 0.0f;
31+
float spawnInterval = 1.2f;
32+
const float obstacleSpeed = 300.0f;
33+
34+
const float minSpawnInterval = 0.8f;
35+
const float maxSpawnInterval = 1.6f;
36+
37+
const int minObsWidth = 40;
38+
const int maxObsWidth = 120;
39+
40+
// --- Game State Variables ---
41+
int score = 0;
42+
bool gameOver = false;
43+
float gameOverTimer = 0.0f;
44+
45+
// --- Back button double press logic ---
46+
float backPressTime = 0.0f;
47+
bool backPressedOnce = false;
48+
const float doublePressInterval = 0.5f; // 0.5 seconds
49+
50+
SetTargetFPS(60);
51+
52+
while (!WindowShouldClose()) {
53+
float dt = GetFrameTime();
54+
55+
// --- Back button exit logic ---
56+
if (backPressedOnce) {
57+
backPressTime += dt;
58+
if (backPressTime > doublePressInterval) {
59+
backPressedOnce = false;
60+
}
61+
}
62+
63+
if (IsKeyPressed(KEY_BACK)) {
64+
if (backPressedOnce) {
65+
break; // Exit game
66+
} else {
67+
backPressedOnce = true;
68+
backPressTime = 0.0f;
69+
}
70+
}
71+
72+
73+
if (!gameOver) {
74+
// Jump logic
75+
if (GetTouchPointCount() > 0 && player.y + player.height >= groundY) {
76+
vy = jumpImpulse;
77+
}
78+
vy += gravity * dt;
79+
player.y += vy * dt;
80+
if (player.y + player.height > groundY) {
81+
player.y = groundY - player.height;
82+
vy = 0;
83+
}
84+
85+
// Spawn obstacles with random width & interval
86+
spawnTimer += dt;
87+
if (spawnTimer >= spawnInterval) {
88+
spawnTimer = 0.0f;
89+
// recalc next interval
90+
spawnInterval = GetRandomValue(int(minSpawnInterval*100), int(maxSpawnInterval*100)) / 100.0f;
91+
// random width
92+
int w = GetRandomValue(minObsWidth, maxObsWidth);
93+
obstacles.push_back({ float(screenW), float(groundY - 40), float(w), 40.0f });
94+
}
95+
96+
// Move & collide obstacles
97+
for (int i = 0; i < (int)obstacles.size(); i++) {
98+
obstacles[i].x -= obstacleSpeed * dt;
99+
if (CheckCollisionRecs(player, obstacles[i])) {
100+
gameOver = true;
101+
gameOverTimer = 0.0f; // Reset timer on game over
102+
}
103+
}
104+
// Remove off-screen & score
105+
if (!obstacles.empty() && obstacles.front().x + obstacles.front().width < 0) {
106+
obstacles.erase(obstacles.begin());
107+
score++;
108+
}
109+
}
110+
else {
111+
gameOverTimer += dt;
112+
// Want to wait 2 seconds before accepting the restart
113+
if (GetTouchPointCount() > 0 && gameOverTimer > 1.0f) {
114+
// reset everything
115+
player.y = screenH - 80;
116+
vy = 0;
117+
obstacles.clear();
118+
spawnTimer = 0.0f;
119+
spawnInterval = 1.2f;
120+
score = 0;
121+
gameOver = false;
122+
}
123+
}
124+
125+
// --- Drawing ---
126+
BeginDrawing();
127+
ClearBackground(RAYWHITE);
128+
129+
DrawRectangle(0, groundY, screenW, 20, DARKGRAY);
130+
DrawRectangleRec(player, BLUE);
131+
for (auto &obs : obstacles) DrawRectangleRec(obs, RED);
132+
133+
DrawText(TextFormat("Score: %d", score), 10, 10, 20, BLACK);
134+
135+
if (gameOver) {
136+
DrawText("GAME OVER! Tap to restart", 200, screenH/2 - 20, 20, MAROON);
137+
}
138+
139+
if (backPressedOnce) {
140+
const char *msg = "Press back again to exit";
141+
int textWidth = MeasureText(msg, 20);
142+
DrawText(msg, (screenW - textWidth) / 2, screenH - 420, 20, BLACK);
143+
}
144+
145+
EndDrawing();
146+
}
147+
148+
CloseWindow();
149+
return 0;
150+
}
151+
152+
} // extern "C"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
buildscript {
2+
repositories {
3+
google()
4+
mavenCentral()
5+
}
6+
dependencies {
7+
classpath 'com.android.tools.build:gradle:8.2.0'
8+
}
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
3+
from test.examples_tools import run
4+
5+
# ############# Example ################
6+
print("- Use the Android NDK to cross-build a package -")
7+
8+
9+
ndk_path = os.environ.get("ANDROID_NDK")
10+
if ndk_path:
11+
print(f"Using Android NDK at: {ndk_path}")
12+
run("gradle --no-daemon assembleDebug")
13+
assert os.path.exists(os.path.join("app", "build", "outputs", "apk", "debug", "app-debug.apk"))
14+
else:
15+
print("WARNING: Skipping Android example, ANDROID_NDK environment variable not set")

0 commit comments

Comments
 (0)