Skip to content
Open
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
11 changes: 11 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
root: true,
extends: "@react-native",
overrides: [
{
files: ['app.plugin.js'],
env: {node: true},
parserOptions: {requireConfigFile: false},
},
],
};
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/lib

.yarn/
.expo/
.gradle/
.cxx/
.DS_Store
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ruby 3.2.0
nodejs 22.11.0
nodejs 22.13.0
java oracle-21.0.3
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,36 @@ Born React Native Godot is distributed on npm.

Just follow these steps to add it to your React Native application:

## Compatibility

The current example is aligned with Expo SDK 57, React Native 0.86 and React 19.2. Node.js follows React Native's supported ranges (`^20.19.4`, `^22.13.0`, `^24.3.0` or `>=25`); this repository uses Node.js 22.13 as its development baseline. The example uses the New Architecture, Hermes and `react-native-worklets` 0.10, which is the Worklets version selected by Expo SDK 57.

Expo monorepos are supported through Expo's automatic Metro configuration. Keep the example/app package inside the workspace list and base its Metro configuration on `expo/metro-config`; custom `watchFolders`, `resolver.nodeModulesPaths` and `disableHierarchicalLookup` settings are not required on Expo SDK 52 or newer. In this repository the library itself is the workspace root, so the example links it with Yarn's `portal:../` protocol.

The Android integration requires Android 10 (API 29) because window embedding uses `SurfaceControl`. The bundled Godot binaries currently support `armeabi-v7a` and `arm64-v8a`; the example restricts `reactNativeArchitectures` to those ABIs so its APK never advertises an x86 variant that cannot load Godot.

## Update `package.json`

```sh
yarn add @borndotcom/react-native-godot
npx expo install @borndotcom/react-native-godot react-native-worklets
```

Expo's install command adds the package config plugin automatically. The plugin keeps clean prebuilds on Android API 29 and the supported ARM ABIs without an `expo-build-properties` entry. To bundle Godot packs on iOS, pass their filenames to that plugin as shown in the example app's `app.json`.

If the dependency was added manually (for example through a Git URL or a workspace), register the package plugin explicitly:

```typescript
export default {
expo: {
plugins: ['@borndotcom/react-native-godot'],
},
};
```

This is the only Android configuration the package needs. Do not duplicate its minimum SDK or add a relative LibGodot Maven path with `expo-build-properties`.

For a React Native Community CLI project, use your package manager instead and ensure the Worklets Babel plugin is enabled according to the `react-native-worklets` installation guide.

## Download the prebuilt LibGodot packages

The LibGodot packages used by React Native Godot are not distributed on npm. Instead, they are downloaded separately by issuing the following command:
Expand All @@ -128,6 +152,10 @@ yarn download-prebuilt

This way React Native Godot can be updated independently from LibGodot, and also local, customized builds of LibGodot are supported.

On Expo, the package config plugin derives the downloaded LibGodot Maven repository from its installed location and writes `android.extraMavenRepos` automatically. Do not add that property or an `expo-build-properties` entry manually. The generated path works with hoisted dependencies, workspaces and regular `node_modules` layouts; React Native Community CLI builds retain the native Gradle fallback.

The bundled Android LibGodot artifacts currently contain `arm64-v8a` and `armeabi-v7a`. The Gradle module automatically limits its native build to those ABIs even when Expo requests x86 variants for other dependencies. Running the Godot view itself therefore requires an ARM Android device or ARM emulator.

## Import React Native Godot in your App code

```typescript
Expand Down Expand Up @@ -372,7 +400,7 @@ iface.test_callable(function(s: string) {

In a React Native app, the main JavaScript thread, where the bulk of the JavaScript code of the application runs is separate from the Android or iOS apps's main thread.

This way the JS Thread's processing does not affect the main application UI. Following the same pattern, the Godot Engine is also running on its own thread that is separate from both the application's and React Native's main JavaScript thread. As JavaScript is single-threaded by design, to be able to communicate with the Godot thread from JavaScript, we use the well-known [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core) library, which allows us running JS code in the Godot thread using worklets.
This way the JS Thread's processing does not affect the main application UI. Following the same pattern, the Godot Engine is also running on its own thread that is separate from both the application's and React Native's main JavaScript thread. As JavaScript is single-threaded by design, to be able to communicate with the Godot thread from JavaScript, we use [react-native-worklets](https://docs.swmansion.com/react-native-worklets/), which allows us to run JS code in the Godot thread using worklets.

Worklets are JavaScript functions designated with a 'worklet' keyword.

Expand All @@ -382,7 +410,7 @@ function worklet() {
}
```

These functions and all their external dependencies are transpiled into self contained JS bundles so they can be executed in separate JS contexts associated with separate threads. For more information on how this works, please refer to the [React Native Worklets Core documentation](https://github.com/margelo/react-native-worklets-core/blob/main/docs/USAGE.md).
These functions and all their external dependencies are transformed so they can be executed in separate JS contexts associated with separate threads. For more information, refer to the [React Native Worklets documentation](https://docs.swmansion.com/react-native-worklets/).

React Native Godot provides a helper function called `runOnGodotThread()` which will allow you to execute such _workletized_ JS functions on the Godot thread.

Expand Down
139 changes: 88 additions & 51 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,37 @@ def safeAppExtGet(prop, fallback) {
appProject?.ext?.has(prop) ? appProject.ext.get(prop) : fallback
}

def libGodotSupportedAbis() {
return ["armeabi-v7a", "arm64-v8a"]
}

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "arm64-v8a"]
if (project.ext.has("rtngodotArchitectures")) {
return project.ext.get("rtngodotArchitectures")
}

def availableAbis = libGodotSupportedAbis()
def requested = project.getProperties().get("reactNativeArchitectures")
def requestedAbis = requested ? requested.split(",")*.trim() : availableAbis
def supportedAbis = requestedAbis.findAll { availableAbis.contains(it) }
def unsupportedAbis = requestedAbis - supportedAbis

if (!unsupportedAbis.isEmpty()) {
logger.lifecycle(
"[RTNGodot] Ignoring unsupported Android ABIs ${unsupportedAbis}. " +
"The bundled LibGodot binaries support ${availableAbis}."
)
}

if (supportedAbis.isEmpty()) {
throw new GradleException(
"[RTNGodot] None of the requested Android ABIs (${requestedAbis}) are supported. " +
"Use one of ${availableAbis}."
)
}

project.ext.set("rtngodotArchitectures", supportedAbis)
return supportedAbis
}

def resolveBuildType() {
Expand All @@ -20,18 +48,33 @@ def resolveBuildType() {
def resolveReactNativeDirectory() {
def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null)
if (reactNativeLocation != null) {
return file(reactNativeLocation)
return file(reactNativeLocation).canonicalFile
}

// monorepo workaround
// react-native can be hoisted or in project's own node_modules
def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native")
if (reactNativeFromProjectNodeModules.exists()) {
return reactNativeFromProjectNodeModules
def command = [
"node",
"--print",
"require.resolve('react-native/package.json', { paths: [process.argv[1], process.argv[2]] })",
projectDir.absolutePath,
rootProject.projectDir.absolutePath,
]
def process = command.execute(null, projectDir)
def stdout = new StringBuilder()
def stderr = new StringBuilder()
process.consumeProcessOutput(stdout, stderr)
def exitCode = process.waitFor()

if (exitCode == 0) {
def packageJson = file(stdout.toString().trim())
if (packageJson.exists()) {
return packageJson.parentFile.canonicalFile
}
}

throw new GradleException(
"[RTNGodot] Unable to resolve react-native location in node_modules. You should project extension property (in `app/build.gradle`) `REACT_NATIVE_NODE_MODULES_DIR` with path to react-native."
"[RTNGodot] Unable to resolve react-native from ${projectDir}. " +
"Set the app extension property REACT_NATIVE_NODE_MODULES_DIR when using a non-standard package layout. " +
stderr.toString().trim()
)
}

Expand All @@ -48,12 +91,6 @@ def isNewArchitectureEnabled() {

def reactNativeRootDir = resolveReactNativeDirectory()

def reactProperties = new Properties()
file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }

def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")
def REACT_NATIVE_MINOR_VERSION = REACT_NATIVE_VERSION.startsWith("0.0.0-") ? 1000 : REACT_NATIVE_VERSION.split("\\.")[1].toInteger()

def prefabHeadersDir = project.file("$buildDir/prefab-headers/rtngodot")

def getPrebuiltLibraryVersion(libName) {
Expand All @@ -76,6 +113,31 @@ def getPrebuiltLibraryVersion(libName) {

def libGodotVersion = getPrebuiltLibraryVersion("libgodot-android")
def godotCppVersion = getPrebuiltLibraryVersion("libgodot-cpp-android")
def libGodotMavenRepo = file("$projectDir/libs/libgodot-android/${libGodotVersion}").canonicalFile

if (!libGodotMavenRepo.isDirectory()) {
throw new GradleException(
"[RTNGodot] Bundled LibGodot Maven repository was not found at ${libGodotMavenRepo}. " +
"Run the package's download-prebuilt command before building Android."
)
}

// Maven repositories are not transitive. Register the repository bundled with this
// package directly on every consumer project, using this module's real location so
// hoisted dependencies, workspaces, portals, and conventional node_modules layouts
// all resolve without android.extraMavenRepos or expo-build-properties.
rootProject.allprojects { targetProject ->
if (targetProject.repositories.findByName("libgodot") == null) {
targetProject.repositories.maven {
name = "libgodot"
url = uri(libGodotMavenRepo)
content {
includeGroup("com.migeran.libgodot")
}
}
}

}

buildscript {
ext.safeExtGet = {prop, fallback ->
Expand Down Expand Up @@ -104,7 +166,6 @@ android {
buildFeatures {
prefab true
prefabPublishing true
buildConfig true
}

// If it doesn't exist
Expand All @@ -120,11 +181,11 @@ android {


defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 29)
// SurfaceControl is the foundation of the embedded window implementation
// and is only available from Android 10 (API 29).
minSdkVersion 29
targetSdkVersion safeExtGet('targetSdkVersion', 33)
ndkVersion safeExtGet('ndkVersion', "26.1.10909125")
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
buildConfigField("String", "FLAVOR", "\"rtngodot\"") // Just a placeholder value.
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your app.
Expand All @@ -134,7 +195,6 @@ android {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared",
"-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}",
"-DANDROID_TOOLCHAIN=clang",
"-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}",
"-DIS_NEW_ARCHITECTURE_ENABLED=${isNewArchitectureEnabled().toString()}",
Expand Down Expand Up @@ -177,7 +237,6 @@ android {
"**/libreact_render_graphics.so",
"**/libreact_render_imagemanager.so",
"**/libreact_render_mapbuffer.so",
"**/libreact_render_debug.so",
"**/libreact_utils.so",
"**/librrc_image.so",
"**/librrc_view.so",
Expand All @@ -195,44 +254,22 @@ android {
}
}

/**
* Task: registerLibGodotRepo
*
* Adds a Maven repository named 'libgodot' that points to the locally bundled pre‑built
* Godot Android artifacts. The repository URL is relative to this library module:
*
* libs/libgodot-android/${libGodotVersion}/
*
* The task is executed before any compile/assemble tasks so that the consuming app can resolve
* `com.migeran.libgodot:godot-dev` (or similar) from the local directory.
*/
task registerLibGodotRepo {
// Capture the version at configuration time
def libVersion = libGodotVersion
def repoUrl = uri("$projectDir/libs/libgodot-android/${libVersion}/")
rootProject.allprojects { proj ->
// Avoid adding the same repo multiple times
if (!proj.repositories.any { it instanceof MavenArtifactRepository && it.name == 'libgodot' }) {
proj.repositories.maven {
name = 'libgodot'
url = repoUrl
}
print("Added local Maven repository 'libgodot' to project ${proj.path}: ${repoUrl}\n")
}
}
}


repositories {
maven {
name = "libgodot"
url = uri(libGodotMavenRepo)
content {
includeGroup("com.migeran.libgodot")
}
}
mavenCentral()
google()
}

dependencies {
implementation 'com.facebook.react:react-native'
implementation project(':react-native-worklets-core')
implementation project(':react-native-worklets')
api "com.migeran.libgodot:godot-debug:${libGodotVersion}-SNAPSHOT"
implementation('com.jakewharton.threetenabp:threetenabp:1.4.9')
}

task deleteCmakeCache() {
Expand Down
2 changes: 1 addition & 1 deletion android/fix-prefab.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// From: react-native-worklets-core/android/fix-prefab.gradle
// From: react-native-worklets/android/fix-prefab.gradle

tasks.configureEach { task ->
// Make sure that we generate our prefab publication file only after having built the native library
Expand Down
13 changes: 6 additions & 7 deletions android/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,14 @@ if(NOT ${CMAKE_BUILD_TYPE} MATCHES "Debug")
endif()

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "")

# Prefab packages from React Native
find_package(fbjni REQUIRED CONFIG)
add_library(fbjni ALIAS fbjni::fbjni)

find_package(ReactAndroid REQUIRED CONFIG)
add_library(jsi ALIAS ReactAndroid::jsi)
add_library(reactnative ALIAS ReactAndroid::reactnative)

# Consume shared libraries and headers from prefabs

find_package(react-native-worklets-core REQUIRED CONFIG)
find_package(react-native-worklets REQUIRED CONFIG)

add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../build/generated/source/codegen/jni" generated)
set(LIBGODOT_CPP_VERSION ${GODOT_CPP_VERSION})
Expand All @@ -51,6 +46,10 @@ file(GLOB rtngodot_SRC CONFIGURE_DEPENDS *.cpp)
file(GLOB rtngodot_common_SRC CONFIGURE_DEPENDS ../../../../common/*.cpp)
add_library(rtngodot SHARED ${rtngodot_SRC} ${rtngodot_common_SRC})

# Android 15+ devices may use 16 KiB memory pages. Keep this module loadable
# there instead of relying on the OS page-size compatibility mode.
target_link_options(rtngodot PRIVATE "-Wl,-z,max-page-size=16384")

target_include_directories(rtngodot PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
"../../../../common"
Expand All @@ -61,7 +60,7 @@ target_link_libraries(
fbjni::fbjni
ReactAndroid::jsi
ReactAndroid::reactnative
react-native-worklets-core::rnworklets
react-native-worklets::worklets
react_codegen_RTNGodotSpec
)

Expand Down
Loading