Bridging Platforms: Unity-Java Integration via JAR Files for Cross-Platform Development

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:

  1. Error Handling: Implement robust error handling on both sides
  2. Memory Management: Properly dispose of Java objects
  3. Threading: Handle async operations carefully
  4. Testing: Test thoroughly on target Android devices
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper