Skip to main content

Command Palette

Search for a command to run...

Create Game Apps Using Godot Engine in Expo with react-native-godot

Updated
19 min read
Create Game Apps Using Godot Engine in Expo with react-native-godot
A

Software Engineer

Introduction

With the release of @borndotcom/react-native-godot, React Native developers can now embed a full Godot Engine runtime directly inside their apps. This unlocks a new class of hybrid apps: React Native UI on top, Godot rendering underneath.

For the first time, you can:

  • Use React Native for UI, navigation, and app features

  • Use Godot for rendering, physics, input, or full game scenes

  • Ship both platforms (iOS & Android) from a single project.

In this guide, I’ll show you how to set up @borndotcom/react-native-godot inside an Expo app and run a Godot 2D game with React Native UI controls layered on top. We’ll walk through:

  • how to set up Godot within your Expo project

  • how to add your Godot assets and configure them to bundle correctly for iOS and Android using Expo's config plugin system

  • how to render the GodotView component in your React Native code and wire up controls that communicate between the two layers

By the time we're finished, you'll have a fully functional Expo app running Godot game on both iOS and Android.

Setting Up Godot in Your Expo Project

Create an Expo Project

If you don’t already have an Expo project, you can create one using the Expo CLI. This guide uses a simple Default template, but you can choose any.

pnpm create expo-app --template

? Choose a template: › - Use arrow-keys. Return to submit.
❯   Default - includes tools recommended for most app developers
    Blank
    Blank (TypeScript)
    Navigation (TypeScript)
    Blank (Bare)

Once created, navigate into the project:

cd godotdemo

Install Required Dependencies

Along with @borndotcom/react-native-godot we’ll install a few additional libraries used in this guide:

  • expo-build-properties – to configure Android-specific settings.

  • expo-file-system – for handling file system paths from React Native.

  • @expo/vector-icons – optional, for building UI controls.

npx expo install expo-build-properties expo-file-system @expo/vector-icons
pnpm add -D expo-module-scripts
pnpm install @borndotcom/react-native-godot

Download the prebuilt LibGodot packages

@borndotcom/react-native-godot depends on LibGodot packages. These packages are not distributed on npm and must be downloaded separately.

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

Setup the download script

Open your project’s package.json and add a script like:

{
  "scripts": {
    "download-prebuilt": "download-prebuilt"
  }
}

Then run:

pnpm run download-prebuilt

This downloads the correct LibGodot binaries for both iOS and Android.

Optional: Automate it with postinstall

To make future installs easier, you can automatically download LibGodot every time someone installs dependencies. Add this to your package.json:

{
  "scripts": {
    "download-prebuilt": "download-prebuilt",
    "postinstall": "pnpm run download-prebuilt"
  }
}

Set orientation

If your game is designed for landscape mode set the app orientation in app.json:

{
    "expo": {
        "orientation": "landscape"
    }
}

Configure Android Build Properties

Before generating native folders, make sure your Android settings are correct. Add this to your app.json / app.config.js:

{
    "expo": {
      "plugins": [
        [
            "expo-build-properties",
            {
              "android": {
                "minSdkVersion": 29,
                "extraMavenRepos": [
                  "../../node_modules/@borndotcom/react-native-godot/android/libs/libgodot-android/4.5.1.migeran.2"
                ]
              }
            }
          ]
      ]
    }
  }

This configuration:

  • Sets the required minSdkVersion = 29

  • Registers the LibGodot local Maven repo, so Android builds can resolve engine binaries.

Generating the iOS and Android Folders

If this is a brand-new Expo project, you may not see ios or android folders yet. To create these native folders (so config plugins can attach and assets can be bundled correctly), run:

npx expo prebuild

Now that the project is fully set up, we can add the Godot game assets that will be bundled into the iOS and Android builds.

Add Your Godot Game Assets

Now that we have the basic setup in place, let's talk about getting your actual Godot game content into your Expo project. iOS and Android handle game assets differently and understanding why will help you make sense of the setup.

When you export a game from Godot you have two options for how to package it as a single pack file (with a .pck extension) or as a folder containing all your game's individual files. Both approaches work, but each platform has different performance characteristics that make one approach better than the other.


To keep this guide focused on integration rather than building a Godot game we’ll use pre-exported Godot assets. You can replace them with your own.

You can download the exact assets used in this guide from:

Demo Assets on GitHub

💡
The sample platformer assets used in this demo are not created by me. they come from the Brackeys Godot tutorial and were repackaged/modified by Brackeys. (credit / more info in the repo)

iOS Asset Setup

For your iOS build you'll want to place your exported Godot game as a pack file. Create a godot folder inside your project’s assets directory then add your file:

assets/godot/main.pck

iOS handles pack files efficiently so using a pack file is actually the better choice. It's cleaner easier to manage, and doesn't sacrifice any performance.

Android Asset Setup

As per the @borndotcom/react-native-godot documentation:

💡
On Android, inside the main package the access of the pack file’s contents is much slower than accessing pack files stored in the private area of the application. If the Godot app is stored inside the main package, then it should be stored as a folder of files in the asset folder.

Because of this, for Android we’ll use the exported folder of files from Godot instead of a single .pck file.

Create a godot-files folder inside the godot directory we created in the previous setup, then place your exported files folder inside it

assets/godot/godot-files/main/

This is where your Android Godot game export should live.

Final Folder Structure

At this point, your project should have the Godot assets placed correctly for both iOS and Android. Here’s what the expected structure looks like:

assets/
  godot/
    main.pck                 # iOS build uses the .pck file
    godot-files/             # Android build uses unpacked files
      main/
        (your exported Godot files here)

With the assets now in place, the next step is to configure the Expo config plugins so these files are included correctly in your iOS and Android builds.

Expo Config Plugin — Copy Godot Assets to Native Folders

At this point, you have your Godot assets in your Expo project assets directory, but there's an important step that needs to happen those assets need to copy into the native iOS and Android project folders when your app prebuilds. Expo doesn't automatically move files from your project assets into the native folders which is where config plugins come in.

A config plugin allows you to modify the native iOS and Android projects that Expo generates during the prebuild process in Continuous Native Generation (CNG) projects.

For Android, this is relatively straightforward. The plugin copies your godot-files folder into the Android main package assets directory at android/app/src/main/assets/. Once the files are there, Android knows how to bundle them with your app and make them accessible at runtime.

For iOS however, there's a critical detail you need to understand. The plugin copies your main.pck file into the root of the iOS project, but that's only half the story. In iOS development, simply having a file present in your project folder is not enough. The file also needs to be added to the "main app target". You can think of the target as a list that tells Xcode which files should actually be included when it compiles and packages your app. If a file is not in the target it won't be bundled with your app even if it's present there in the project folder.

Creating the Folder Structure

First, create a folder named plugin in your project root. Inside that folder, create a src directory this is where all of TypeScript plugin files will live.

Structure should look like this:

plugin/
  src/
    index.ts
    withGodotFiles.ts       // Android
    withPckFile.ts          // iOS
  tsconfig.json

Add the Plugin Code

With the folder structure in place the next step is to create the three plugin files and add the code that will copy Godot assets into the native projects during Expo’s prebuild step.

Inside plugin/src/, create the following files:

index.ts

This is the entry point for the plugin. It runs the Android and iOS plugin together during prebuild.

import { ConfigPlugin } from "expo/config-plugins";
import withGodotFiles from "./withGodotFiles";
import withPckFile from "./withPckFile";

const withPlugin: ConfigPlugin = (config) => {
  // Copy Godot files to Android assets
  config = withGodotFiles(config);
  // Copy main.pck to iOS project
  return withPckFile(config);
};

export default withPlugin;

withGodotFiles.ts (Android)

This plugin copies Godot exported files into the Android app’s native assets folder:

import { ConfigPlugin, withDangerousMod } from "expo/config-plugins";
import * as fs from "fs";
import * as path from "path";

/**
 * Recursively copy a directory
 */
function copyDirectory(src: string, dest: string): void {
  // Create destination directory if it doesn't exist
  if (!fs.existsSync(dest)) {
    fs.mkdirSync(dest, { recursive: true });
  }

  // Read all files and directories from source
  const entries = fs.readdirSync(src, { withFileTypes: true });

  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);

    if (entry.isDirectory()) {
      // Recursively copy subdirectories
      copyDirectory(srcPath, destPath);
    } else {
      // Copy files
      fs.copyFileSync(srcPath, destPath);
    }
  }
}

/**
 * Expo config plugin to copy godot-files/main folder to Android assets
 * @param config - Expo config
 */
const withGodotFiles: ConfigPlugin = (config) => {
  return withDangerousMod(config, [
    "android",
    async (config) => {
      const projectRoot = config.modRequest.projectRoot;

      // Source path: assets/godot/godot-files/main
      const sourcePath = path.join(
        projectRoot,
        "assets",
        "godot",
        "godot-files",
        "main"
      );

      // Destination path: android/app/src/main/assets/main
      const destPath = path.join(
        projectRoot,
        "android",
        "app",
        "src",
        "main",
        "assets",
        "main"
      );

      // Check if source directory exists
      if (!fs.existsSync(sourcePath)) {
        console.warn(`Godot files not found at ${sourcePath}`);
        return config;
      }

      // Ensure the assets directory exists
      const assetsDir = path.join(
        projectRoot,
        "android",
        "app",
        "src",
        "main",
        "assets"
      );
      if (!fs.existsSync(assetsDir)) {
        fs.mkdirSync(assetsDir, { recursive: true });
      }

      // Copy the directory
      try {
        copyDirectory(sourcePath, destPath);
        console.log(`Copied Godot files from ${sourcePath} to ${destPath}`);
      } catch (error) {
        console.error(`Error copying Godot files: ${error}`);
      }

      return config;
    },
  ]);
};

export default withGodotFiles;

withPckFile.ts (iOS)

This plugin copies main.pck file into the iOS project and adds it to the Xcode main app target which is required for it to be bundled in the final app.

import { ConfigPlugin, withXcodeProject } from "expo/config-plugins";
import * as fs from "fs";
import * as path from "path";

/**
 * Expo config plugin to copy main.pck file from assets/godot to iOS project
 * @param config - Expo config
 */
const withPckFiles: ConfigPlugin = (config) => {
  return withXcodeProject(config, async (config) => {
    const projectRoot = config.modRequest.projectRoot;
    const project = config.modResults;

    // Source path: assets/godot/main.pck
    const sourcePath = path.join(projectRoot, "assets", "godot", "main.pck");

    // Destination path: ios/main.pck (at the root of ios folder)
    const destPath = path.join(projectRoot, "ios", "main.pck");

    // Check if source file exists
    if (!fs.existsSync(sourcePath)) {
      console.warn("main.pck not found at assets/godot/main.pck");
      return config;
    }

    // Copy the file
    fs.copyFileSync(sourcePath, destPath);
    console.log("Copied main.pck to iOS project");

    // Check if file already exists in the project
    const existingFile = project.hasFile("main.pck");
    if (existingFile) {
      console.log("main.pck already exists in Xcode project");
      return config;
    }

    // Get the main group (root group of the project)
    const firstProject = project.getFirstProject();
    const mainGroupKey = firstProject.firstProject.mainGroup;

    if (!mainGroupKey) {
      console.error("Could not find main group");
      return config;
    }

    console.log("Main group key:", mainGroupKey);

    // Add the file reference with proper encoding
    const file = project.addFile("main.pck", mainGroupKey, {
      lastKnownFileType: "file",
      defaultEncoding: 4,
    });

    if (!file) {
      console.log("Could not add file - it may already exist");
      return config;
    }

    console.log("Added file reference:", file.fileRef);

    // Get target UUID
    const targetUuid = project.getFirstTarget().uuid;
    console.log("Target UUID:", targetUuid);

    // Generate UUID for build file
    const buildFileUuid = project.generateUuid();

    // Manually add to PBXBuildFile section with target
    project.addToPbxBuildFileSection({
      uuid: buildFileUuid,
      isa: "PBXBuildFile",
      fileRef: file.fileRef,
      basename: "main.pck",
      group: "Resources",
    });

    console.log("Added to PBXBuildFile section");

    // Add to Resources build phase with target
    project.addToPbxResourcesBuildPhase({
      uuid: buildFileUuid,
      basename: "main.pck",
      group: "Resources",
      target: targetUuid,
    });

    console.log("Added to Resources build phase with target");

    return config;
  });
};

export default withPckFiles;

plugin/tsconfig.json

{
    "extends": "expo-module-scripts/tsconfig.plugin",
    "compilerOptions": {
      "outDir": "build",
      "rootDir": "src"
    },
    "include": ["./src"],
    "exclude": ["**/__mocks__/*", "**/__tests__/*"]
  }

Build the Plugin

Before Expo can use config plugin, you need to compile it from TypeScript to JavaScript. To do that add a script to your package.json:

{
  "scripts": {
    "build:plugin": "tsc --build plugin/tsconfig.json"
  }
}

Now run:

pnpm build:plugin

This will generate the compiled JavaScript files inside plugin/build/ which Expo will load during the prebuild process.

Configure the Plugin

Create an app.plugin.js file in the root directory and add below code:

module.exports = require("./plugin/build");

Open app.config.js (or app.json) and plugin under the plugins field:

{
  "expo": {
    "plugins": [
      "./app.plugin.js",
      ...
      ..
    ]
  }
}

Run Prebuild

With the plugin setup, the final step is to run Expo’s prebuild process. This is where your plugin actually executes and copies the Godot assets into the native project folders.

npx expo prebuild

After this completes, you should see main.pck copied into the iOS project and unpacked Godot export copied into android/app/src/main/assets/main/

Android

iOS

That’s it for the native side. Config plugin is in place and your Godot assets will always be in the right place for both iOS and Android no manual steps needed.

Next, we’ll look at how to render your Godot scene inside React Native and interact with it.

Bringing Your Godot Game to Life in React Native

Now comes the exciting part actually getting your Godot game running inside your React Native app. Before we can render anything on screen, we need to initialize the Godot engine itself.

Create the Godot instance

The initialization process looks slightly different on iOS and Android and that's because of how each platform handles the game files we set up earlier.

On Android remember how we added all those Godot export files into the /main directory inside android/app/src/main/assets/ Well Android will load the game directly from that unpacked directory structure.

On iOS it loads from that single .pck file we copied into the app bundle earlier. We need to tell the Godot engine the exact path to that file which is where the Expo FileSystem module comes in handy. It helps us construct the correct file path so the engine knows where to find the packed game data.

Let's write the code that handles this initialization.

We'll import the Expo FileSystem module along with our RTNGodot module, and then we'll call the initialization function with the appropriate path based on which platform we're running on. Here's what that looks like:

import { Platform } from "react-native";
import { RTNGodot, runOnGodotThread } from "@borndotcom/react-native-godot";
import * as FileSystem from "expo-file-system/legacy";

function initGodot() {
  // We need to run this code on Godot's own thread, not the main JavaScript thread
  // That's what runOnGodotThread does for us
  runOnGodotThread(() => {
    "worklet"; // This tells the system: this function needs to run on a different thread

    if (Platform.OS === "android") {
      // Android: Point to the unpacked game files in the assets folder
      RTNGodot.createInstance([
        "--verbose", // Enable detailed logging
        "--path",
        "/main", // Load from /main directory
        "--rendering-driver",
        "opengl3",
        "--rendering-method",
        "gl_compatibility",
        "--display-driver",
        "embedded", // required to embed Godot into the React Native application.
      ]);
    } else {
      // iOS: Point directly to the main.pck file in the app bundle
      RTNGodot.createInstance([
        "--verbose",
        "--main-pack",
        FileSystem.bundleDirectory + "main.pck", // Full path to .pck file
        "--rendering-driver",
        "opengl3",
        "--rendering-method",
        "gl_compatibility",
        "--display-driver",
        "embedded",
      ]);
    }
  });
}

Notice we're wrapping our initialization code inside runOnGodotThread. Here's why that matters. In React Native, JavaScript code runs on its own thread, completely separate from the native UI thread. This is actually a good thing because it means JavaScript logic doesn't freeze the app's interface. Following that same pattern the Godot engine also runs on its own dedicated thread keeping it independent from both the native UI and your JavaScript code.

JavaScript by itself is single-threaded which means it can only do one thing at a time on one thread. So how do we get our JavaScript code to talk to the Godot thread? That's where the concept of "worklets" comes in. A worklet is just a JavaScript function that you mark with the 'worklet' keyword at the top. This special keyword tells the system to package up that function and all the code it depends on into a self-contained bundle that can be executed on a different thread.

When we call runOnGodotThread and pass it a worklet function it saying take this chunk of JavaScript and run it over there on Godot's thread, not here on the main JavaScript thread. This is the safest way to interact with the Godot engine because it keeps everything in the right place. The Godot engine expects certain operations to happen on its own thread and by using worklets we're respecting that expectation.

The initialization itself is pretty straightforward. We're passing an array of command-line arguments to RTNGodot.createInstance just like you would if you were launching Godot from a terminal.

Add the Godot View

To show your Godot scene in the app we just render the RTNGodotView component inside our layout. This is the view where your Godot game will appear.

import {
  RTNGodotView,
} from "@borndotcom/react-native-godot";
import { View } from "react-native";

export default function Index() {
  return (
    <View
      style={{
        flex: 1,
      }}
    >
      <RTNGodotView style={{ flex: 1 }} />
    </View>
  );
}

This puts your Godot game on the screen as a full-screen view. Because it behaves like any other React Native component you can easily layer buttons or other UI elements on top of it.

Call the Initialization When the App Loads

Now that we have our initGodot() function and the RTNGodotView on the screen we just need to trigger the initialization when the component mounts. The easiest way to do that is with a useEffect.

export default function Index() {
  useEffect(() => {
    initGodot();
    return () => {};
  }, []);
  return (
    <View
      style={{
        flex: 1,
      }}
    >
      <RTNGodotView style={{ flex: 1 }} />
    </View>
  );
}

At this point, we’ve initialized Godot and added the RTNGodotView, so the game should be able to load. Go ahead and run the app to make sure everything is working.

pnpm run ios
# or
pnpm run android

Running on iOS

Running on Android

Control the Game

With the game now rendering inside your React Native app, the next step is to actually play it. Because the Godot scene is running inside RTNGodotView we can place any React Native UI on top like buttons, joysticks whatever we want.

For this example, we’ll add a simple three-button control layout:

  • Left (move left)

  • Right (move right)

  • Jump

Build the Control UI

Below is the UI setup for the three on-screen buttons (left, right, and jump), positioned on top of the Godot view:

import { Ionicons } from "@expo/vector-icons";
import { StyleSheet, TouchableOpacity, View } from "react-native";
import {
  RTNGodotView
} from "@borndotcom/react-native-godot";

export default function Index() {
  useEffect(() => {
    initGodot();
    return () => {};
  }, []);

  return (
    <View style={styles.container}>
      <RTNGodotView style={styles.gameView} />

      {/* Left side controls - Direction buttons */}
      <View style={styles.leftControls}>
        <TouchableOpacity
          style={styles.button}
          onPressIn={() => {}}
          onPressOut={() => {})}
          activeOpacity={0.7}
        >
          <Ionicons name="chevron-back" size={32} color="white" />
        </TouchableOpacity>
        <TouchableOpacity
          style={styles.button}
          onPressIn={() => {}}
          onPressOut={() => {}}
          activeOpacity={0.7}
        >
          <Ionicons name="chevron-forward" size={32} color="white" />
        </TouchableOpacity>
      </View>

      {/* Right side controls - Jump button */}
      <View style={styles.rightControls}>
        <TouchableOpacity
          style={[styles.button, styles.jumpButton]}
          onPressIn={() => {}}
          onPressOut={() => {}}
          activeOpacity={0.7}
        >
          <Ionicons name="arrow-up" size={36} color="white" />
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  gameView: {
    flex: 1,
  },
  leftControls: {
    position: "absolute",
    bottom: 40,
    left: 30,
    flexDirection: "row",
    gap: 20,
  },
  rightControls: {
    position: "absolute",
    bottom: 40,
    right: 30,
  },
  button: {
    width: 70,
    height: 70,
    borderRadius: 35,
    backgroundColor: "rgba(0, 0, 0, 0.6)",
    justifyContent: "center",
    alignItems: "center",
    borderWidth: 3,
    borderColor: "rgba(255, 255, 255, 0.3)",
  },
  jumpButton: {
    width: 80,
    height: 80,
    borderRadius: 40,
    backgroundColor: "rgba(220, 38, 38, 0.7)",
    borderColor: "rgba(255, 255, 255, 0.4)",
  },
});

Here’s the layout we’ll use for the on-screen controls:

Handle the Button Actions

Now that we've got our buttons on screen we need to make them actually do something. This is where things get interesting because we're building a bridge between two different worlds React Native UI and the Godot game engine running underneath. Let's break down how this communication happens.

How Godot Handles Input

First, let's look at how your Godot game is expecting to receive input. In your GDScript code you've probably got something like this for handling movement:

var direction := Input.get_axis("ui_left", "ui_right")

This single line is doing something clever. The Input.get_axis() function looks at two actions in this case "ui_left" and "ui_right" and returns a value between -1 and 1. If the left action is pressed you get -1. If the right action is pressed you get 1. If neither is pressed you get 0. This makes it really easy to handle movement in your game logic.

For jumping you're probably checking if an action was just pressed like this:

if Input.is_action_just_pressed("ui_accept") and is_on_floor():
    velocity.y = JUMP_VELOCITY

This checks if the "ui_accept" action was pressed this frame and if the character is standing on the ground. If both conditions are true, the character jumps.

These actions come straight from the player.gd script in the Godot project. That’s where the character’s movement and jump input are defined.

Building the Bridge from React Native

So how do we trigger these Godot input actions from our React Native buttons? We need two helper functions: one to tell Godot hey, this button was just pressed and another to say okay, the button was released. Here's what those look like:

function pressAction(action: string) {
  runOnGodotThread(() => {
    "worklet";
    try {
      const Godot = RTNGodot.API();
      const Input = Godot.Input;
      Input.action_press(action);
    } catch (error) {
      console.error("Error pressing action:", error);
    }
  });
}

function releaseAction(action: string) {
  runOnGodotThread(() => {
    "worklet";
    try {
      const Godot = RTNGodot.API();
      const Input = Godot.Input;
      Input.action_release(action);
    } catch (error) {
      console.error("Error releasing action:", error);
    }
  });
}

Both functions follow the same pattern we saw earlier with initialization they use runOnGodotThread to make sure the code runs on Godot's dedicated thread, not the main JavaScript thread.

Inside each function, we're accessing the Godot API and specifically grabbing the Input singleton, which is Godot's global input manager. When we call Input.action_press(action) we're telling Godot simulate pressing this action right now. When we call Input.action_release(action) we're saying simulate releasing it.

Wiring Up the Buttons

When you create your React Native buttons you hook up these functions to the touch events:

<TouchableOpacity
  onPressIn={() => pressAction("ui_left")}
  onPressOut={() => releaseAction("ui_left")}
>
  <Ionicons name="chevron-back" size={32} color="white" />
</TouchableOpacity>

When someone touches the left arrow onPressIn fires and calls pressAction("ui_left"). This tells Godot that the left action is now pressed. In GDScript Input.get_axis("ui_left", "ui_right") immediately starts returning -1 and your character begins moving left. When they lift their finger onPressOut fires calling releaseAction("ui_left") and the movement stops.

The same pattern works for the right button with "ui_right" and the jump button with "ui_accept". The beauty of this approach is that from Godot's perspective it doesn't matter whether the input is coming from a physical keyboard or from your React Native buttons it all looks the same to the Input singleton.

At this point you should be able to move left, move right, and jump directly from your RN UI while the game runs inside the RTNGodotView.

Managing the Godot Instance (Stop, Pause, Resume)

Before wrapping up it’s worth mentioning that you’re not locked into a single running Godot instance. The engine gives you full control over its lifecycle which is useful if you ever want to show a menu screen, change levels, or switch to a different Godot entirely.

Stop the Godot instance

If you want to shut the engine down completely, you can destroy the active instance:

function destroyGodot() {
  runOnGodotThread(() => {
    "worklet";
    RTNGodot.destroyInstance();
  });
}

Stopping the instance fully unloads the Godot runtime. You can later start a new instance even with a different Godot project by calling createInstance() again.

Pause the instance

If you just want to temporarily stop the game (for example when opening a fullscreen game settings menu) you can pause execution:

RTNGodot.pause();

This freezes the running scene without destroying it.

Resume the instance

And when you’re ready to continue:

RTNGodot.resume();

This simply continues the gameplay where it left off.

Wrapping Up

And that’s it you now have a full Godot game running inside an Expo app, complete with platform-specific asset handling, a working Expo config plugin, and real-time controls built using plain React Native UI.

If you’d like to explore the complete setup including the config plugin, Godot assets, and the React Native controls I’ve shared the full demo project on GitHub.