Unity's powerful game engine combined with Java's robust ecosystem creates a formidable combination for cross-platform development. By integrating Java code through JAR files, Unity applications can leverage native Android APIs, enterprise libraries, and existing Java codebases. This article explores various techniques for seamless Unity-Java integration, from basic method calls to complex bidirectional communication.
Why Integrate Unity with Java?
Common Use Cases:
- Accessing native Android APIs (GPS, sensors, notifications)
- Using existing Java libraries and SDKs
- Integrating with enterprise backend systems
- Implementing platform-specific functionality
- Leveraging Java's rich ecosystem in Unity games
Architecture Overview:
Unity (C#) → Android Java Bridge → JAR Library → Native Android APIs
Method 1: Android Java Native Interface (JNI)
Basic Unity-to-Java Communication:
1. Create the Java Library (JAR)
AndroidPlugin.java:
package com.yourcompany.unityplugin;
import android.app.Activity;
import android.content.Context;
import android.widget.Toast;
import android.util.Log;
public class AndroidPlugin {
private static final String TAG = "UnityJavaPlugin";
private static Activity unityActivity;
// Method to set the Unity activity context
public static void setUnityActivity(Activity activity) {
unityActivity = activity;
Log.d(TAG, "Unity activity set");
}
// Simple method call from Unity
public static String getPlatformInfo() {
return "Android Platform - Unity Java Integration";
}
// Method with parameters
public static int calculateSum(int a, int b) {
return a + b;
}
// Method that shows Android Toast
public static void showToast(final String message) {
if (unityActivity != null) {
unityActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(unityActivity, message, Toast.LENGTH_LONG).show();
}
});
}
}
// Method returning complex data as JSON
public static String getDeviceInfo() {
if (unityActivity == null) return "{}";
Context context = unityActivity.getApplicationContext();
return String.format(
"{\"model\":\"%s\", \"sdk\":%d, \"manufacturer\":\"%s\"}",
android.os.Build.MODEL,
android.os.Build.VERSION.SDK_INT,
android.os.Build.MANUFACTURER
);
}
// Async method with callback
public static void processDataAsync(final String data, final DataCallback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// Simulate processing
Thread.sleep(1000);
String result = "Processed: " + data.toUpperCase();
callback.onSuccess(result);
} catch (Exception e) {
callback.onError(e.getMessage());
}
}
}).start();
}
// Callback interface for async operations
public interface DataCallback {
void onSuccess(String result);
void onError(String error);
}
}
2. Create the Unity C# Bridge
AndroidJavaBridge.cs:
using System;
using UnityEngine;
public class AndroidJavaBridge : MonoBehaviour
{
private static AndroidJavaClass unityClass;
private static AndroidJavaObject unityActivity;
private static AndroidJavaClass pluginClass;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
if (Application.platform == RuntimePlatform.Android)
{
InitializeAndroid();
}
}
private static void InitializeAndroid()
{
try
{
unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
unityActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
pluginClass = new AndroidJavaClass("com.yourcompany.unityplugin.AndroidPlugin");
// Set the Unity activity in the Java plugin
pluginClass.CallStatic("setUnityActivity", unityActivity);
Debug.Log("Android Java Bridge initialized successfully");
}
catch (System.Exception e)
{
Debug.LogError($"Failed to initialize Android Java Bridge: {e.Message}");
}
}
// Simple method call
public static string GetPlatformInfo()
{
if (pluginClass != null)
{
return pluginClass.CallStatic<string>("getPlatformInfo");
}
return "Not on Android platform";
}
// Method with parameters
public static int CalculateSum(int a, int b)
{
if (pluginClass != null)
{
return pluginClass.CallStatic<int>("calculateSum", a, b);
}
return -1;
}
// Method that shows Toast
public static void ShowToast(string message)
{
if (pluginClass != null)
{
pluginClass.CallStatic("showToast", message);
}
}
// Method returning JSON data
public static string GetDeviceInfo()
{
if (pluginClass != null)
{
return pluginClass.CallStatic<string>("getDeviceInfo");
}
return "{}";
}
// Async method call
public static void ProcessDataAsync(string data, Action<string> onSuccess, Action<string> onError)
{
if (pluginClass != null)
{
// Create callback implementation in Java
AndroidJavaObject callback = new AndroidJavaObject("com.yourcompany.unityplugin.AndroidPlugin$DataCallback")
{
// Success callback
Call("onSuccess", new object[] { onSuccess.Target }),
// Error callback
Call("onError", new object[] { onError.Target })
};
pluginClass.CallStatic("processDataAsync", data, callback);
}
}
}
3. Usage in Unity Scene
DemoController.cs:
using UnityEngine;
using UnityEngine.UI;
public class DemoController : MonoBehaviour
{
[SerializeField] private Text resultText;
[SerializeField] private Button testButton;
[SerializeField] private Button toastButton;
[SerializeField] private Button asyncButton;
private void Start()
{
testButton.onClick.AddListener(TestJavaIntegration);
toastButton.onClick.AddListener(ShowToastMessage);
asyncButton.onClick.AddListener(TestAsyncOperation);
// Initial test
TestJavaIntegration();
}
private void TestJavaIntegration()
{
string platformInfo = AndroidJavaBridge.GetPlatformInfo();
int sumResult = AndroidJavaBridge.CalculateSum(15, 25);
string deviceInfo = AndroidJavaBridge.GetDeviceInfo();
resultText.text = $"Platform: {platformInfo}\n" +
$"Sum Result: {sumResult}\n" +
$"Device Info: {deviceInfo}";
}
private void ShowToastMessage()
{
AndroidJavaBridge.ShowToast("Hello from Unity!");
}
private void TestAsyncOperation()
{
resultText.text = "Processing data...";
AndroidJavaBridge.ProcessDataAsync(
"hello world",
result => {
// Success callback
resultText.text = $"Async Result: {result}";
AndroidJavaBridge.ShowToast("Processing completed!");
},
error => {
// Error callback
resultText.text = $"Error: {error}";
AndroidJavaBridge.ShowToast("Processing failed!");
}
);
}
}
Method 2: Advanced JAR Integration with Callbacks
Complex Java Library with Callbacks:
AdvancedAndroidPlugin.java:
package com.yourcompany.unityplugin;
import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
public class AdvancedAndroidPlugin {
private static Activity unityActivity;
private static SensorManager sensorManager;
private static Sensor accelerometer;
private static LocationManager locationManager;
private static UnityCallback sensorCallback;
private static UnityCallback locationCallback;
public static void setUnityActivity(Activity activity) {
unityActivity = activity;
// Initialize sensors
sensorManager = (SensorManager) activity.getSystemService(Activity.SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// Initialize location
locationManager = (LocationManager) activity.getSystemService(Activity.LOCATION_SERVICE);
}
// Sensor management
public static void startAccelerometer(final UnityCallback callback) {
sensorCallback = callback;
SensorEventListener sensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
String data = String.format("{\"x\":%.2f,\"y\":%.2f,\"z\":%.2f}", x, y, z);
callback.onDataReceived(data);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Handle accuracy changes if needed
}
};
sensorManager.registerListener(sensorListener, accelerometer,
SensorManager.SENSOR_DELAY_NORMAL);
}
public static void stopAccelerometer() {
sensorManager.unregisterListener((SensorEventListener) sensorManager);
sensorCallback = null;
}
// Location services
public static void startLocationUpdates(final UnityCallback callback) {
locationCallback = callback;
try {
LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
String data = String.format(
"{\"lat\":%.6f,\"lon\":%.6f,\"accuracy\":%.1f}",
location.getLatitude(),
location.getLongitude(),
location.getAccuracy()
);
callback.onDataReceived(data);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {}
@Override
public void onProviderEnabled(String provider) {}
@Override
public void onProviderDisabled(String provider) {}
};
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
1000, // 1 second
1, // 1 meter
locationListener
);
} catch (SecurityException e) {
callback.onError("Location permission denied");
}
}
public static void stopLocationUpdates() {
locationManager.removeUpdates((LocationListener) locationManager);
locationCallback = null;
}
// File operations
public static String readFile(String filename) {
// Implementation for reading files
return "File content placeholder";
}
public static boolean writeFile(String filename, String content) {
// Implementation for writing files
return true;
}
// Network operations
public static void makeHttpRequest(String url, final UnityCallback callback) {
new Thread(() -> {
try {
// Simulate network request
Thread.sleep(2000);
String response = "{\"status\":\"success\",\"data\":\"Response from " + url + "\"}";
new Handler(Looper.getMainLooper()).post(() -> {
callback.onDataReceived(response);
});
} catch (Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
callback.onError("Network error: " + e.getMessage());
});
}
}).start();
}
// Callback interface for Unity
public interface UnityCallback {
void onDataReceived(String data);
void onError(String error);
}
}
Advanced Unity Bridge:
AdvancedAndroidBridge.cs:
using System;
using UnityEngine;
public class AdvancedAndroidBridge : MonoBehaviour
{
private static AndroidJavaClass advancedPluginClass;
public static event Action<string> OnSensorDataReceived;
public static event Action<string> OnLocationDataReceived;
public static event Action<string> OnNetworkDataReceived;
public static event Action<string> OnErrorOccurred;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
if (Application.platform == RuntimePlatform.Android)
{
InitializeAdvancedAndroid();
}
}
private static void InitializeAdvancedAndroid()
{
try
{
var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
var unityActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
advancedPluginClass = new AndroidJavaClass("com.yourcompany.unityplugin.AdvancedAndroidPlugin");
advancedPluginClass.CallStatic("setUnityActivity", unityActivity);
Debug.Log("Advanced Android Bridge initialized");
}
catch (Exception e)
{
Debug.LogError($"Advanced Android Bridge init failed: {e.Message}");
}
}
// Sensor methods
public static void StartAccelerometer()
{
if (advancedPluginClass != null)
{
var callback = new AndroidJavaObject("com.yourcompany.unityplugin.AdvancedAndroidPlugin$UnityCallback");
callback.Call("onDataReceived", new object[] {
new Action<string>(data => OnSensorDataReceived?.Invoke(data))
});
callback.Call("onError", new object[] {
new Action<string>(error => OnErrorOccurred?.Invoke(error))
});
advancedPluginClass.CallStatic("startAccelerometer", callback);
}
}
public static void StopAccelerometer()
{
advancedPluginClass?.CallStatic("stopAccelerometer");
}
// Location methods
public static void StartLocationUpdates()
{
if (advancedPluginClass != null)
{
var callback = new AndroidJavaObject("com.yourcompany.unityplugin.AdvancedAndroidPlugin$UnityCallback");
callback.Call("onDataReceived", new object[] {
new Action<string>(data => OnLocationDataReceived?.Invoke(data))
});
callback.Call("onError", new object[] {
new Action<string>(error => OnErrorOccurred?.Invoke(error))
});
advancedPluginClass.CallStatic("startLocationUpdates", callback);
}
}
public static void StopLocationUpdates()
{
advancedPluginClass?.CallStatic("stopLocationUpdates");
}
// Network methods
public static void MakeHttpRequest(string url)
{
if (advancedPluginClass != null)
{
var callback = new AndroidJavaObject("com.yourcompany.unityplugin.AdvancedAndroidPlugin$UnityCallback");
callback.Call("onDataReceived", new object[] {
new Action<string>(data => OnNetworkDataReceived?.Invoke(data))
});
callback.Call("onError", new object[] {
new Action<string>(error => OnErrorOccurred?.Invoke(error))
});
advancedPluginClass.CallStatic("makeHttpRequest", url, callback);
}
}
}
Method 3: Building and Deploying JAR Files
Maven Build Configuration:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.yourcompany</groupId> <artifactId>unity-android-plugin</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Android SDK dependency --> <dependency> <groupId>com.google.android</groupId> <artifactId>android</artifactId> <version>4.1.1.4</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <outputDirectory>../UnityProject/Assets/Plugins/Android</outputDirectory> </configuration> </plugin> </plugins> </build> </project>
Gradle Alternative:
build.gradle:
plugins {
id 'java-library'
}
group 'com.yourcompany'
version '1.0.0'
sourceCompatibility = 1.8
repositories {
google()
jcenter()
}
dependencies {
compileOnly 'com.google.android:android:4.1.1.4'
}
jar {
destinationDirectory.set(file("../UnityProject/Assets/Plugins/Android"))
}
Method 4: Unity Plugin Management
Unity Plugin Structure:
Assets/ ├── Plugins/ │ └── Android/ │ ├── YourPlugin.jar │ ├── AndroidManifest.xml │ └── libs/ │ └── additional-libs.jar
AndroidManifest.xml for Unity:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yourcompany.unityplugin"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@mipmap/app_icon" android:label="@string/app_name"> <activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Method 5: Bidirectional Communication
Java-to-Unity Communication:
UnityMessageHandler.java:
package com.yourcompany.unityplugin;
import com.unity3d.player.UnityPlayer;
public class UnityMessageHandler {
private static final String UNITY_GAME_OBJECT = "UnityJavaBridge";
// Send message to Unity
public static void sendToUnity(String methodName, String parameter) {
UnityPlayer.UnitySendMessage(UNITY_GAME_OBJECT, methodName, parameter);
}
public static void onSensorData(String data) {
sendToUnity("OnSensorDataReceived", data);
}
public static void onLocationUpdate(String locationData) {
sendToUnity("OnLocationUpdateReceived", locationData);
}
public static void onNetworkResponse(String response) {
sendToUnity("OnNetworkResponseReceived", response);
}
public static void onError(String errorMessage) {
sendToUnity("OnJavaError", errorMessage);
}
}
Unity Receiver:
UnityMessageReceiver.cs:
using UnityEngine;
public class UnityMessageReceiver : MonoBehaviour
{
private void Start()
{
// This GameObject must be named exactly as specified in Java code
if (gameObject.name != "UnityJavaBridge")
{
Debug.LogWarning("GameObject should be named 'UnityJavaBridge' for Java communication");
}
}
// Called from Java
public void OnSensorDataReceived(string data)
{
Debug.Log($"Sensor data from Java: {data}");
// Process sensor data
}
// Called from Java
public void OnLocationUpdateReceived(string locationData)
{
Debug.Log($"Location update from Java: {locationData}");
// Process location data
}
// Called from Java
public void OnNetworkResponseReceived(string response)
{
Debug.Log($"Network response from Java: {response}");
// Process network response
}
// Called from Java
public void OnJavaError(string errorMessage)
{
Debug.LogError($"Java error: {errorMessage}");
// Handle Java-side errors
}
}
Method 6: Error Handling and Debugging
Robust Error Handling:
ErrorHandlingBridge.cs:
using System;
using UnityEngine;
public class ErrorHandlingBridge : MonoBehaviour
{
public static T SafeJavaCall<T>(Func<T> javaCall, T defaultValue, string operationName)
{
try
{
if (Application.platform != RuntimePlatform.Android)
{
Debug.LogWarning($"{operationName}: Not on Android platform");
return defaultValue;
}
return javaCall();
}
catch (Exception e)
{
Debug.LogError($"{operationName} failed: {e.Message}");
return defaultValue;
}
}
public static void SafeJavaCall(Action javaCall, string operationName)
{
try
{
if (Application.platform != RuntimePlatform.Android)
{
Debug.LogWarning($"{operationName}: Not on Android platform");
return;
}
javaCall();
}
catch (Exception e)
{
Debug.LogError($"{operationName} failed: {e.Message}");
}
}
// Example usage
public static string GetDeviceInfoSafe()
{
return SafeJavaCall(
() => AndroidJavaBridge.GetDeviceInfo(),
"{}",
"GetDeviceInfo"
);
}
}
Java-Side Logging:
DebugLogger.java:
package com.yourcompany.unityplugin;
import android.util.Log;
public class DebugLogger {
private static final String TAG = "UnityJavaPlugin";
private static boolean enableLogging = true;
public static void setLoggingEnabled(boolean enabled) {
enableLogging = enabled;
}
public static void logDebug(String message) {
if (enableLogging) {
Log.d(TAG, message);
}
}
public static void logError(String message) {
Log.e(TAG, message);
}
public static void logWarning(String message) {
Log.w(TAG, message);
}
public static void logInfo(String message) {
if (enableLogging) {
Log.i(TAG, message);
}
}
}
Best Practices and Performance Optimization
1. Object Pooling for Java Objects:
public class JavaObjectPool
{
private static Dictionary<string, AndroidJavaObject> objectPool =
new Dictionary<string, AndroidJavaObject>();
public static AndroidJavaObject GetJavaObject(string key, Func<AndroidJavaObject> creator)
{
if (!objectPool.ContainsKey(key))
{
objectPool[key] = creator();
}
return objectPool[key];
}
public static void ClearPool()
{
foreach (var obj in objectPool.Values)
{
obj.Dispose();
}
objectPool.Clear();
}
}
2. Async Operations with Coroutines:
public class AsyncJavaOperations : MonoBehaviour
{
public static AsyncJavaOperations Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public void ExecuteJavaOperationAsync(Action operation, Action onComplete = null)
{
StartCoroutine(ExecuteOperationCoroutine(operation, onComplete));
}
private System.Collections.IEnumerator ExecuteOperationCoroutine(Action operation, Action onComplete)
{
var thread = new System.Threading.Thread(() => operation());
thread.Start();
while (thread.IsAlive)
{
yield return null;
}
onComplete?.Invoke();
}
}
3. Memory Management:
public class JavaMemoryManager : MonoBehaviour
{
private List<AndroidJavaObject> managedObjects = new List<AndroidJavaObject>();
public AndroidJavaObject CreateManagedObject(string className, params object[] args)
{
var obj = new AndroidJavaObject(className, args);
managedObjects.Add(obj);
return obj;
}
private void OnDestroy()
{
foreach (var obj in managedObjects)
{
obj?.Dispose();
}
managedObjects.Clear();
}
}
Testing and Debugging
Unit Test Framework:
JavaPluginTester.cs:
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
public class JavaPluginTester
{
[UnityTest]
public IEnumerator TestJavaBridgeInitialization()
{
// Arrange
bool initializationSuccess = false;
// Act
yield return new WaitForSeconds(1f); // Allow initialization
try
{
string platformInfo = AndroidJavaBridge.GetPlatformInfo();
initializationSuccess = !string.IsNullOrEmpty(platformInfo);
}
catch (System.Exception)
{
initializationSuccess = false;
}
// Assert
Assert.IsTrue(initializationSuccess, "Java bridge should initialize successfully");
}
[Test]
public void TestJavaMethodCalls()
{
if (Application.platform != RuntimePlatform.Android)
{
Assert.Inconclusive("Test only runs on Android");
return;
}
// Test simple method call
string result = AndroidJavaBridge.GetPlatformInfo();
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Android"));
// Test method with parameters
int sum = AndroidJavaBridge.CalculateSum(10, 20);
Assert.AreEqual(30, sum);
}
}
Conclusion
Unity-Java integration via JAR files provides powerful capabilities:
Key Benefits:
- Native API Access: Leverage Android-specific functionality
- Code Reuse: Integrate existing Java libraries and SDKs
- Performance: Execute computationally intensive tasks in native code
- Flexibility: Create custom plugins for specific needs
Best Practices:
- Error Handling: Implement robust error handling on both sides
- Memory Management: Properly dispose of Java objects
- Threading: Handle async operations carefully
- Testing: Test thoroughly on target Android devices
- Documentation: Document plugin APIs and usage
Common Pitfalls to Avoid:
- Forgetting to set Unity activity context
- Not handling platform differences
- Memory leaks from undisposed Java objects
- Blocking the main thread with Java calls
- Inadequate error handling for edge cases
By following these patterns and best practices, you can create robust, high-performance Unity applications that seamlessly integrate with Java libraries and Android native functionality, providing the best of both worlds for cross-platform development.