How to embed Unity 3D in a native Android app
I‘m currently working on a project that requires embedding Unity 3D in a native Android app. In this article I’ll tell you what I’ve learned, so you can do this if you need to.
Here is a short list of what you’ll find in this article:
- export a Unity project as an aar library;
- embed the aar in the native Android project;
- two way communication between Android and Unity;
- back button and device rotation support;
- embedding Unity in a Fragment.
At the moment of writing this, I’m using Android Studio 3.4.1 and Unity 2019.1.7f1.
Exporting the Unity project as an aar library
The Unity project used in this example is a simple spinning cube. By pressing a native Android Button, we will be able to increase the cube’s spinning speed.
There is only one script in this project. It is called CubeRotate and it handles the cube rotation and cube-related communication with the native app.
I’ll provide details for both of these scripts later in this tutorial. For now let’s focus on exporting our Unity project.
After pressing File > Build Settings we must make sure that we have the Android platform selected and that Export Project is checked. By having this checked, Unity will export our project as a native Android app with an Activity, Manifest, Gradle and all that. For reference you can checkout the screenshot below.
We’ll also need to add a package name to the project. We do this by pressing the Player Settings button from the Build Settings window (lower left in the above screenshot), select the Android icon and scroll down to Other Settings.
As you can see in the above screenshot, my package name is “com.bittreat.unityplugintest” but you should add your own
Now we can press that Export button from the Build Settings window (in the lower right).
It’s time to import the project into Android Studio. To do this, we press File> New and choose the folder where we exported our Unity project. In a pop-up asking me if I should use the project SDK or Android SDK I chose to use Android SDK (which is newer than the one of the Unity project). I’m not sure if this choice incurs any compatibility issues but I encountered no problems so far.
Afterwards I encountered a pop-up asking me if I want to update Gradle and I pressed Update.
What we have now, is a regular Unity App. You can run the project and it will run like any Android Unity app. You can look up the UnityPlayerActivity and see a few of the things that happen behind the scenes in a Unity Android project.
For our purposes we don’t need an Android app, but an aar library which we can embed in our native Android app. To do that, we’ll delete all the intent-filter code from the AndroidManifest.xml file. I deleted this part:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Afterwards, in the build.gradle file, we need to change the line:
apply plugin: 'com.android.application'
into
apply plugin: 'com.android.library'
Next, I removed the line:
applicationId 'com.bittreat.unityplugintest'
and also removed the lines:
bundle {
language {
enableSplit = false
}
density {
enableSplit = false
}
abi {
enableSplit = true
}
}
Afterwards I built the project and went to build/outputs/aar where I found the generated aar. I renamed it into unity-plugin.aar but this is just an optional step, there is no need to do so. Just remember that if you chose to name it something else, from now on everywhere you see me use “unity-plugin” you should replace it with the name of your own aar file.
Finally, after all these steps, we now have a unity-plugin.aar file that we can use in our native Android app.
Embedding the aar in the native project
We now need to create our simple native Android app. This app has three activities: MainActivity, MiddleActivity and UnityActivity.
The MainActivity has a simple button with the ‘Hello World’ text on it. The MiddleActivity is there to help us with the back button and rotation action (more on that later). Finally, the UnityActivity extends the UnityPlayerActivity and this is the single activity in the app where we see our Unity project in action.
In order to import unity-plugin.aar into the project, we press File > New > New Module and scroll to the bottom of the list where we’ll find “Import .JAR/.AAR Package”.
In the next step, we are prompted to find the path to our unity-plugin.aar location. After that, Android Studio managed all the required gradle stuff. In case that, for some reason, things don’t happen automatically on your side, you will have to add:
implementation project(":unity-plugin")
to the dependencies in your gradle app module. Also have your settings.gradle file look like:
include ':app', ':unity-plugin'
At this point, trying to run the project, I encountered a manifest merger failure. This can be solved by adding the line:
xmlns:tools="http://schemas.android.com/tools"
to your <manifest> tag, and
tools:replace="android:icon,android:theme"
to your <application> tag. After all that we are able to build our project.
At this point you can start running the Unity Activity by simply using startActivity(intent). In my case I used the three activities described above.
Pressing the ‘Hello World’ button starts the MiddleActivity (more on that in Back button and rotation section) and the MiddleActivity starts the UnityActivity.
Calling Unity code from the native Android app
According to the Unity documentation, we can call C# methods from our native project by using the method:
UnitySendMessage("GameObjectName1", "MethodName1", "Message to send");
As we can see, it has 3 string parameters. The first one is the name of the GameObject to which the script is attached. The second parameter is the name of the method (from that script) that we want to call. And the last one is the parameter of the method. We can only call Unity methods with no parameters or with a string parameter.
In our Unity project we have a cube with the name Cube that has a script named CubeRotate attached. To make it easier to follow I kept only the parts of the script that interests us at the moment. You’ll see the rest later. For now here is how it looks like:
public class CubeRotate : MonoBehaviour {
private float speed = 40f;
private void Update() {
gameObject.transform.Rotate(speed * Time.deltaTime,
speed * Time.deltaTime, 10f * Time.deltaTime);
}
public void CallFromAndroidWithNoMessage() {
speed = speed * 2;
}
public void CallFromAndroidWithMessage(string value) {
Debug.Log($"Coming from Android: {value}");
}
}
As you can see in the Update() method our cube will rotate continuously with speed speed. There are two other methods CallFromAndroidWithNoMessage() which doubles the rotation speed and CallFromAndroidWithMessage(string value) which just displays a message with the value in the console.
Now back to Android. To keep things separated, I created a class called CommunicationBridge in which I keep all the back and forth communication code between the Android app and the Unity plugin. In this class we have (for now) these two methods:
fun callToUnityWithNoMessage() {
UnityPlayer.UnitySendMessage("Cube",
"CallFromAndroidWithNoMessage", "")
}
fun callToUnityWithMessage(param: String) {
UnityPlayer.UnitySendMessage("Cube",
"CallFromAndroidWithMessage", param)
}
By calling the first method we trigger the C# CallFromAndroidWithNoMessage method. As you can see, we use UnitySendMessage with “Cube” as its first parameter (remember that’s the name of our GameObject), the C# method name as its second parameter and an empty string as the third parameter because the C# method has no parameters.
By calling the second method we see pretty similar things except that this time we specify a third parameter.
In the onCreate method of UnityActivity I added the following code:
val button = Button(this)
button.layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT)
button.text = "Call with no message"
button.setOnClickListener {
if(toggle) {
button.text = "Call with Message"
communicationBridge.callToUnityWithNoMessage()
} else {
button.text = "Call with no message"
communicationBridge.callToUnityWithMessage("Hello Unity!")
}
toggle = !toggle
}
button.setBackgroundColor(Color.GREEN)
button.setTextColor(Color.RED)
mUnityPlayer.addView(button)
What that does, is just adding a button with a toggle so that it alternately calls the two methods from communicationBridge.
We can see the cube rotation speed doubling when pressing the button when its text is “Call with no message” and we can see the text “Coming from Android: Hello Unity!” displayed in logcat when the button text is “Call with Message”.
Calling Android code from Unity
Before heading to Unity, let’s add some code to the CommunicationBridge class. The complete code is:
class CommunicationBridge(
val communicationCallback: CommunicationCallback) {
fun callFromUnityWithNoParameters() {
communicationCallback.onNoParamCall()
}
fun callFromUnityWithOneParameter(param: String) {
communicationCallback.onOneParamCall(param)
}
fun callFromUnityWithTwoParameters(param1: String, param2: Int){
communicationCallback.onTwoParamCall(param1, param2)
}
fun callToUnityWithNoMessage() {
UnityPlayer.UnitySendMessage("Cube",
"CallFromAndroidWithNoMessage", "")
}
fun callToUnityWithMessage(param: String) {
UnityPlayer.UnitySendMessage("Cube",
"CallFromAndroidWithMessage", param)
}
interface CommunicationCallback {
fun onNoParamCall()
fun onOneParamCall(param: String)
fun onTwoParamCall(param1: String, param2: Int)
}
}
The additions include a callback in the constructor and 3 new methods which will be called from Unity.
Now let’s head to Unity and complete the CubeRotate script.
public class CubeRotate : MonoBehaviour {
private AndroidJavaObject communicationBridge;
private float speed = 40f;
private void Start() {
communicationBridge = new AndroidJavaObject(
"com.bittreat.apps.unitycommunicationtest.CommunicationBridge");
}
private void Update() {
gameObject.transform.Rotate(speed * Time.deltaTime,
speed * Time.deltaTime, 10f * Time.deltaTime);
if (Application.platform == RuntimePlatform.Android) {
if (Input.GetKey(KeyCode.Escape)) {
Application.Quit();
}
}
}
public void CallFromAndroidWithNoMessage() {
speed = speed * 2;
}
public void CallFromAndroidWithMessage(string value) {
Debug.Log($"Coming from Android: {value}");
if(speed <= 81f) {
communicationBridge
.Call("callFromUnityWithNoParameters");
speed = 80f;
} else if(speed <= 170f) {
communicationBridge
.Call("callFromUnityWithOneParameter",
"Hello from Unity!");
speed = 160f;
} else if(speed <= 330f) {
communicationBridge
.Call("callFromUnityWithTwoParameters",
"The speed was: ", (int) speed);
speed = 40f;
}
}
}
To communicate with Android, we need to use an AndroidJavaObject. As you can see in the Start() method, it takes a string as a constructor parameter. Note that that string is the full name of the Android target class including its package name. In our case, that is the aforementioned CommunicationBridge class.
We call the Android methods by using the Call method of the AndroidJavaObject. The first parameter of this method is the name of the Android method. Other parameters are optional and they are method parameters.
When the speed is less or equal to 81 we call the method callFromUnityWithNoParameters which has no parameters. When the speed is less or equal to 170 we call callFromUnityWithOneParameter which has a string parameter and lastly we call callFromUnityWithTwoParameters which has a string and an int parameter.
In Android, in the onCreate method of UnityActivity, we instantiate our CommunicationBridge class like this:
val communicationBridge = CommunicationBridge(
object :CommunicationBridge.CommunicationCallback {
override fun onNoParamCall() {
Log.d("UnityCalled", "Callback with no parameter")
}
override fun onOneParamCall(param: String) {
Log.d("UnityCalled",
"Callback with one parameter: $param")
}
override fun onTwoParamCall(param1: String, param2: Int) {
Log.d("UnityCalled",
"Callback with two parameters: $param1, $param2")
}
})
By using the callback we can see (in the Logcat console) which method was called from Unity.
Back button and rotation
Handling the back button can be done in a few ways. The simplest way to do it is to quit the Unity application when pressing the back button. This will quit the current Activity (the UnityActivity) and reveal the previous activity which is the MiddleActivity that finishes itself and moves on to the MainActivity. To do this we write the following lines in a Unity Update method:
if(Input.GetKeyDown(KeyCode.Escape)) {
Application.Quit();
}
A more involved way of handling the back button is to do a callback to Android and handle it there:
if(Input.GetKeyDown(KeyCode.Escape)) {
communicationBridge.Call("androidHardwareBackButton");
}
In this case, androidHardwareBackButton is a method from the CommunicationBridge class. This is a good option for when you need to do some processing on the Android side before quitting the UnityActivity.
It’s finally time for the screen rotation and why do we even need a MiddleActivity. The reason is that when we rotate the device, the UnityActivity gets destroyed. The MiddleActivity is a simple white background Activity (or you can add a progress indicator to it or whatever you like). It launches the UnityActivity after getting rotated.
class MiddleActivity: AppCompatActivity() {
private var activityWasJustCreated = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityWasJustCreated = true
val intent = Intent(this, UnityActivity::class.java)
startActivity(intent)
}
override fun onResume() {
super.onResume()
if(!activityWasJustCreated) {
finish()
}
activityWasJustCreated = false
}
}
When hitting the back button in the UnityActivity, activityWasJustCreated is already set to false so when onResume is called in the MiddleActivity, it finishes, and we get the MainActivity.
It is a little convoluted, but otherwise, if we tried to do this MiddleActivity business in the MainActivity, we would see the MainActivity UI while waiting for the UnityActivity to start after each rotation.
When rotation happens and the UnityActivity is restarted, it doesn’t save the state it had before rotation. If we want to make that happen, we need to save the state on the Android side and have a way to receive state input on the Unity side.
Embedding Unity in a Fragment
Perhaps we need a more complex native UI on top of the UnityActivity. This can be achieved by embedding the Unity part in a Fragment. Note that the Fragment must still be full screen, but we can layout other elements on top of it.
First thing that we need to do, in order to achieve this, is to remove the setContentView() method from the UnityActivity that Unity exports before converting it to an aar library.
After that, in our native project, in the Activity that extends UnityPlayerActivity, we add the Unity parts to a fragment by getting the view from the unityPlayer like:
ourPlaceHolderLayout.addView(unityPlayer.view)
In which ourPlaceHolderLayout is just a child layout in our Activity layout.
Being able to embed Unity in a Fragment leads to many options when it comes to project structure. The way we use it in our real project is having a single Activity for the Unity part and having multiple interchangeable fragments in which we use Unity. The navigation is handled using the Jetpack navigation component. You can see a screenshot of our project below.
Conclusion
In this article I tried to answer all the questions that I had during the development of our project. Hopefully it can answer some of the questions that you may have regarding this subject.
If you have better solutions to the problems this article is trying to solve, please share them.
If you’re curious about the end result of our Unity/Android native project and especially if you have a dog and a passion for Agility, you can checkout out our app both on Android and iOS.
If you liked this post you can find more on our website.
I can't find the attached code repo, is there any? Razvan Soare
Can't understand a point. Unity get a new instance of CommunicationBridge with the "new AndroidJavaObject" and call the methods of this instance that doesn't implements the CommunicationCallback interface, The UnityActivity has another instance of CommunicationBridge that correctly implements the CommunicationCallback interface. When Unity calls the CommunicationBridge methods is not referring to this instance, and throw ad exception because communicationCallback member is null.
Hi, shall we connect want more clarity on this? Will you help me more on this?
This is a long shot, but I'm curious if when implementing the unity callbacks, if you ever had any issues with Android not being responsive to events coming from unity. Even following this implementation directly I never get back to my Activity or Fragment when using the communication bridge callback interface.