With the emergence of excellent cross-platform game engines like Unity and cocos2d-x, developers can free themselves from the laborious development work on Android and iOS Native App and focus their energy on creative game design. Previously, creating a cross-platform game might have required developers to be proficient in Java, Objective-C, C#, or even C/C++. Now, with the help of Unity, developers only need to possess minimal knowledge of Native App development to craft an outstanding game.
Especially at Tencent, with components like Apollo, integrating Native App has become even simpler. It's possible that each project team would only need 1-2 individuals proficient in Android and iOS development. However, due to this convenience, many colleagues have sufficient reasons to avoid learning or engaging with Android and iOS development until they need to integrate. This often leads to encountering pitfalls when they start seeking assistance or references for integration.
The purpose of this article is to provide a foundation in basic Android development knowledge and practical operations. This aims to equip everyone with a certain reservoir of Android fundamental knowledge. Alternatively, this can be used as a foundational guide for integrating Android SDK/plugins into Unity. By following the steps outlined, mistakes can be largely avoided.
This article will start with the familiar Unity to introduce how to integrate self-written or third-party Android plug-ins into your games.
1. How does Unity Package APK Files?
2. Foundations of Android Development and Integration with Unity
If you've looked through the integration documentation of certain third-party components, you're probably aware that Unity has specific folders that are closely tied to packaging APKs. Let's delve into understanding the process these folders undergo to get included in the APK.
Within Unity's Assets directory, the Plugins/Android folder stands out as a crucial component. Let's start by examining what a typical Plugins/Android directory looks like.
The last four entries are related to the Android project. The initial two folders are references to third-party libraries, which are also bundled into the APK. If we navigate into these first two folders, we'll notice that their directory structure is quite similar to the Android directory structure, resembling something like this:
Comparing the directory interfaces of the upper and lower layers, we can find that there are many similar parts, such as libs, res, assets folders, and AndroidManifest.xml files. These are the files required for a standard Android project. The function of the Android packaging tool that comes with Unity is to organize the contents of the above folders in a fixed way and compress them into the APK file.
Next, let's take a closer look at the operations performed by Android packaging tools.
1. There are many *.jar files in the libs folder, and *.so files placed in a folder with a fixed name. *.jar files are files compiled by the Java compiler. When Android is packaged, all jar files in the project will be merged, compressed, and recompiled into classes.dex files, which will be placed in the root directory of the APK. When the application is executed, the Java virtual machine (Dalvik or Art) in the Android system will interpret the bytecode in classes. dex and execute it. Compiling many jar packages into classes.dex files is an indispensable step in packaging Android applications.
At this point, some might wonder if this is accurate. This step only converts .jar files into a dex file. Wasn't the generation of .jar files from Java files done in an earlier step? You're correct. Generally, the .jar files used here are generated by other Android IDEs and then copied over. The later parts of this article will cover how to use Android IDEs and generate the necessary files.
2. The *.so files in the "libs" directory are dynamically loadable library files that can be loaded by the Android system. These files are typically written in C/C++ and compiled into binary format. It's important to note that due to variations in the CPU architectures that execute these binary libraries, the same C/C++ code is usually compiled into different files targeting different CPU architectures. This is why the "libs" folder often contains several fixed directories such as armeabi-v7a, armeabi, x86, and others, each containing .so files with the same naming convention. When the Java Virtual Machine loads these dynamic libraries, it selects the appropriate .so file based on the current CPU architecture. Sometimes, these .so files can be executed on different CPU architectures, albeit with slightly slower performance on non-matching architectures. Therefore, when prioritizing speed, it's advisable to generate specific .so files for each architecture, and when focusing on package size, generating an armeabi .so file would suffice.
3. The "assets" folder contains the simplest content. When packaging an APK, the contents of these files are copied verbatim to the "assets" folder in the root directory of the APK. This folder has several characteristics:
4. The "res" folder typically contains XML files and various image assets. There are several types of XML files commonly found within:
For example, images in the "ldpi" drawable folder generally have the smallest dimensions in the series. The content of this folder is used on devices with the lowest pixel density. On the other hand, phones with resolutions like 1080p, 2k, or 4k will retrieve images from folders labeled "xxxxhdpi" to ensure clear visuals within the app. During the packaging process, image resources are placed in corresponding directories within the APK's "res" folder.
5. In addition to the mentioned XML files in the "res" folder, there are various other common XML files in Android; however, we won't list them all here.
XML files under the "res" folder are transformed into a more efficient binary format during the packaging process. Despite this transformation, they are still named with the ".xml" extension and are placed in the "res" folder inside the APK, maintaining a directory structure corresponding to the original layout.
Apart from converting XML files, Android's packaging tools also establish mappings between resource files in the "res" folder and resources statically referenced in code. This mapping information is then placed in the "resources.arsc" file at the root of the APK. This step ensures that the Android application can load the correct resources when it starts, making it an essential part of the Android app packaging process.
6. The AndroidManifest.xml file holds paramount importance as a guide for the Android system. When installing and launching an Android application, the system primarily reads the contents of this file. It analyzes the fundamental elements used by the application and determines which code segments to retrieve from the classes.dex file, decides where to place the app icon on the home screen, and manages whether the application is debuggable, among other crucial details. Further explanation follows in subsequent sections.
When handling the AndroidManifest.xml file within a Unity project, the packaging tool combines the contents of all such files. This means that if library projects referred to by the main project also have their own AndroidManifest files, they will be merged. This automated merging negates the need for manual copying and pasting.
It's important to note that this file is indispensable when packaging Android applications. During Unity's packaging process, if there is no AndroidManifest.xml file found in the Plugins directory, Unity will use a default AndroidManifest as a replacement. Additionally, Unity automatically checks whether certain information within the project's AndroidManifest is the default value. If they are, Unity replaces those values with the corresponding ones from the Unity project. This includes aspects like the app's name and icon.
7. The project.properties file is typically found within library projects. Its content is minimal, often consisting of just one line: android.library=true. However, the absence of this file could lead the Android packaging tool to overlook the folder as an Android library project. Consequently, during packaging, the entire folder might be ignored. While this might not always disrupt the packaging process or cause immediate errors, it can result in missing resources or code in the generated APK, leading to crashes upon execution.
This aspect of Android development is not extensively detailed in Unity's official documentation, as it pertains to fundamental knowledge about Android projects. This lack of clarity has led many developers new to Unity-Android development to encounter issues here. A seasoned veteran in Unity-based Android game development once advised me to handle Android library dependencies by using Eclipse to open the Plugins/Android folder and sort out all the project dependencies there. However, this practice of coupling Unity projects with Eclipse projects isn't the most optimal, as it can lead to slow performance when launching Unity projects.
8. Other folders such as aidl and jni are typically not involved in the APK generation process within Unity and won't be elaborated on here.
Having grasped the foundational knowledge about Unity's APK packaging that was presented earlier, we now understand the impact of placing certain files in the Plugins/Android directory. However, the provided information has mainly focused on how Unity performs APK packaging. Therefore, the following will briefly outline the steps involved in completing this packaging process.
Android provides a tool called aapt, which stands for Android Asset Packaging Tool. This tool handles most of the tasks related to processing resource files, and Unity utilizes a series of calls to the Android toolchain (Android Build Tools) to accomplish APK packaging. This process is somewhat analogous to creating a bat/bash script that sequentially invokes the tools provided by Android. In some common Android Integrated Development Environments (IDEs), such a "bat/bash script" often forms a complete build system. The original Android IDE was Eclipse, and its build system was based on Ant, utilizing XML-based configuration. Later, the Android team introduced a dedicated IDE called Android Studio (which will be detailed later in the article). Android Studio adopted the Gradle build system, transitioning from XML-based configuration to a language-based approach (DSL, Domain Specific Language). This shift provided Android Studio users with more flexibility.
Reaching this point, I believe many individuals now understand how to integrate the Android SDK/plugins into Unity and package them accordingly. At this juncture, some might be thinking, "Just knowing how to place these files isn't enough; I also need to comprehend how to write Android code and generate the corresponding SDK/plugins for Unity's use."
The upcoming sections of this article will progressively outline the process of crafting Android code and generating library files for Unity.
Once you've installed Android Studio and configured the proxy settings, you can launch the application. In the pop-up dialog, choose "Start a new Android Studio project."
In the upcoming interface, please input the application name, company domain (which is not very crucial, to be honest), and package name. Among these, I consider the package name to be the most significant, as it reveals the developer's level of sophistication.
Next, choose what type of App you want to develop, just tick Phone and Tablet here. The choice of SDK is generally based on the needs of the project, and the minimum is generally not lower than API 9: Android 2.3 (Gingerbread), which is also the minimum SDK acceptable to Unity. If some plug-ins cannot run in such a low Android SDK environment, you can consider upgrading to API 15: Android 4.3 (IceCreamSandwich) as appropriate. This level of API is generally compatible with most machines that have been in the past 3-4 years.
Because the content we want to output is for Unity, here you can choose No Activity (the component that hosts the game visuals), and consider adding it later when needed.
After clicking OK, Android Studio will begin initializing the current Android project. This initialization process might take some time as Android Studio might need to download necessary frameworks or update versions of Android tools. Once the initialization is complete, you can navigate to the left side and follow the steps shown in the image to open the project directory tree, allowing you to view the entire project structure.
From the above screenshot, we can gather that an Android Studio project can consist of several smaller Modules. These modules can include application modules with Activities or library modules with No Activities, among other possibilities. There can be referencing relationships between these modules. We can extract separate modules for foundational functionalities or easily reusable components.
If you want to create a new module, you can right-click in the list shown in the screenshot and select "New Module." In the popup window that appears, you can choose the type of module you want to create. You can also import old projects from Eclipse. For Unity game development plugins, the most commonly used type is an Android Library module (AndroidLibrary). Similarly, in the subsequent window that appears, you'll fill in the module name, package name, and the minimum required SDK for the module.
Take a brief look at the directory structure of the Android project. As shown below:
The libs directory serves the same purpose as described in the first part of this article. Place the required libraries here.
The src/main/res directory functions similarly to the res directory explained in the first part of this article. Place corresponding resources in it.
Next is the directory for Java code, src/main/java. This directory is somewhat special; its subpaths must correspond to the package names defined within the Java files.
AndroidManifest.xml also serves the same purpose as explained in the first part of this article.
The build folder is dynamically generated by Android Studio. The generated APK package (for application modules) or AAR package (for library modules) will be placed within the output folder here. It's important to note that this folder should not be committed to SVN, as doing so could lead to conflicts among project members.
src/test and src/androidTest are used for unit testing purposes, which are not covered in this article.
Now we can start getting hands-on with coding. Here, we'll write an Activity that can display an Android Toast notification, which will replace Unity's default Activity.
Briefly describe the relationship between Unity and Activity: In the Android system, opening an application is to start the activity specified by the application.
There is a default Activity in Unity. Its function is to load Unity's Player when the system starts the application. This Player is equivalent to the "player" of the Unity application. It will execute the content we created in the Unity project, and Render to the specified SurfaceView through GL commands, and SurfaceView is a special View placed in the Activity.
Firstly, in Android Studio, navigate to src/main/java (as shown in the image). Next, right-click and choose "New" followed by "Activity" to create a new "Empty Activity".
In the popup window, provide a name for your Activity that adheres to Java code conventions. Additionally, select an appropriate package name. You can refer to the configuration in the image below:
The "Generate Layout File" option doesn't need to be checked when creating an Activity for a Unity game. Checking the "Launcher Activity" option will prompt Android Studio to automatically declare this Activity as one of the entry points to the app in the AndroidManifest.xml file of the current module. However, for a library project like ours, this option isn't necessary. Once you click "Finish," Android Studio will generate a basic Activity in the designated directory. The contents of this Activity will be as follows:
It's important to highlight that the provided Android Activity is only a fundamental component and won't automatically load our Unity content. To achieve this, we need to make it inherit from Unity's Activity rather than the default one.
Here's the step-by-step process:
1. Begin by locating Unity's installation directory and navigate to the following subdirectory: Editor\Data\PlaybackEngines\AndroidPlayer\Variations\il2cpp\Release\Classes\.
2. Inside this directory, you'll find a file named classes.jar. This jar file contains Unity's default Activity packaged within it. Copy this classes.jar file to the libs directory of your current module. Feel free to rename the jar file for better organization if you wish. (The source code for this jar can be found in the directory Editor\Data\PlaybackEngines\AndroidPlayer\Source\com\unity3d\player. Those interested can explore the source code to gain an understanding of Unity's player loading mechanism.)
3. Next, in Android Studio's Project View, locate your current module, right-click on it, and choose "Open Module Settings." Alternatively, you can directly press F4.
4. In the opened window, navigate to the "Dependencies" tab on the right-hand side, then click on the green plus icon labeled "Jar Dependency."
By following these steps, you'll be able to integrate the Unity-specific jar package into your Android module, allowing your Activity to inherit from Unity's Activity and facilitate the loading of Unity content.
Navigate to the libs folder within your project and locate the recently imported jar package. Click "OK" to confirm its selection. Next, there's a crucial step to follow: we need to modify the scope attribute of this jar package. By default, the scope attribute ("Compile") would merge the contents of this jar package with the Java code in the current module. This could lead to errors when Unity packages this module's jar, as the previously added jar is already embedded within Unity. To avoid this, we need to change the scope attribute to "provided."
Please refer to the image below as a guide to change the jar package's scope to "provided."
Certainly, after removing the first line from the list to prevent the bundling of all jar packages in the libs folder, you can proceed to modify your recently created Activity to inherit from UnityPlayerActivity. Don't forget to import the necessary package. Your modified code should look like this:
Up to this point, if your Activity is runnable, it should be able to utilize the code from its parent class UnityPlayerActivity to run Unity. Now, let's add a method to this Activity that, when called, will display a system default Toast message.
You've rightly pointed out a crucial aspect. The core method here is the one that invokes the Android Toast component, and it's relatively straightforward to understand. On the other hand, the runOnUiThread method on the outside is worth paying attention to. In Android programming, any operations involving UI manipulation must be performed within the UI thread to prevent data modification by other threads from causing crashes. Since the ShowMessage method we've written might be called from Unity, and Unity's invocations might not be on the UI thread, it's essential to protect against potential issues.
In Android, there are various ways to schedule code execution on the UI thread. The use of runOnUiThread in the provided code is the simplest approach. For more complex scenarios, you might consider using techniques like Messenger or Handler to manage threading. Interested individuals can find more information online on these advanced threading concepts in Android programming.
After completing the code writing for the Activity, you can integrate this module into your Unity project. In Android Studio, you can choose "Build" > "Make Project," or in the project view on the left, select the module you want to export, then choose "Build" > "Make Module." After selecting this, you'll notice a Gradle progress bar below. Once the progress bar completes, you can navigate to the build/outputs/aar directory of the module to find the generated output.
Inside this folder, you'll see a file with a .aar extension. This file is the compiled result of the module. If you use decompression software to extract its contents, you'll find it's almost like a complete Android project. As mentioned in the first part of this document, you only need to create a folder within the Plugins/Android directory of your Unity project. After extracting the contents of the .aar file, simply place them into the newly created folder. Additionally, manually create a file named project.properties with the content android.library=true, and place this file within the newly created folder.
Victory is within sight! Now, all we need to do is change the entry point Activity in the AndroidManifest.xml file within the Unity project to our newly written one. It's important to note that in older Unity projects, someone might have already written a relevant AndroidManifest file placed in the Plugins/Android directory. However, in a completely new Unity project, this file might not exist.
During the packaging process, if Unity detects that there's no AndroidManifest file in the Plugins/Android directory, it will copy a default version of the file and modify the relevant project-related content. You can find the AndroidManifest.xml file in the Editor\Data\PlaybackEngines\AndroidPlayer\Apk directory within Unity's installation folder. Copy this file and place it in your Unity project's Plugins/Android directory. Next, you'll need to modify its contents.
Let's explain the key points in this article:
1. package="com.unity3d.player": If left unchanged, this content will be modified by Unity during the packaging process to match the Bundle Identifier specified in the Player Settings.
2. android:versionCode and android:versionName: These two elements will be adjusted during packaging based on the Version and Bundle Version Code specified in the Player Settings.
3. android:icon and android:label: These two attributes correspond to the application's icon and its display name. If not modified, Unity will automatically adjust them according to the content in the Player Settings.
4. android:debuggable="true": During packaging, Unity will automatically modify this attribute based on the Development Build option in the Build Settings.
5. activity android:name: The 'name' attribute within the 'activity' tag specifies the Java Activity class that the activity should run. If not modified, Unity will load the default Unity Activity class. In this article, the default Activity needs to be replaced with our custom implementation. Thus, we need to provide the complete name of the newly created Activity, including the package name and class name.
6. activity android:label: This represents the line of text displayed below the application icon on the home screen, which is the application's name. If not changed, Unity will handle this for you.
7. meta-data: The 'name' value in this line serves as the key, and the 'value' represents the content associated with that key. Multiple meta-data entries can be customized as needed, but key values must not be duplicated. The 'unityplayer.UnityActivity' entry in the code is likely intended for Unity's reference, informing Unity that it operates within this specific Activity.
In this process, our primary focus is to modify the android:name within the activity tag. After making this modification, we can utilize Unity's built-in Build feature to create an Android package. Before proceeding with the packaging, please ensure to check the Bundle Identifier in the Player Settings. It's important not to retain the default package name here, as it could lead to compilation failures. During the compilation process, you might encounter some errors.
Below are a few common errors that you can attempt to address:
1. Manifest File Merging Error: This error usually occurs when merging all AndroidManifest files. Common causes include duplicated activity definitions or incorrect minimum SDK versions. Module minimum SDK versions must not be lower than the project's minimum SDK.
2. Jar File Dex Error: If there are inadvertently multiple identical jar files in your project, you might encounter this error. Remove the duplicates and retain only one copy.
3. Android SDK Tool Not Found: This is generally a Unity bug. Unity often struggles to be compatible with the latest Android SDK tools. In such cases, manual downgrading of the SDK might be necessary.
Apart from the aforementioned issues, there can be various other errors that arise during the Android project packaging process. When encountering such issues, there's no need to panic. Errors are part of the process, and typically, when you encounter an error message, searching it on the all-knowing Google can lead you to solutions.
Up to this point in the article, we've discussed how to package Android plugins within a Unity project. However, we haven't covered how to invoke the code we've written in an Android Activity from within Unity. This step is quite straightforward for a Unity developer, as Unity provides the AndroidJavaClass and AndroidJavaObject classes to act as intermediaries for data interchange between Unity and Java. Calling methods and accessing fields through these classes gives the impression of invoking Java code via reflection. As long as you have access to a Java object using the package name and class name, you can directly call Java code on the other side using method names or variable names.
Let's take an example:
Suppose we want to call the ShowMessage class from the code we wrote earlier in Unity. To achieve this, we need to prepare the following code in Unity:
Key Points in the Code
1. You can conveniently obtain the Java object instance of the current Activity using UnityPlayer.
2. Calling methods on Java object instances is quite straightforward. The Call method is used to invoke methods on the Java side.
3. Pay attention to using macros to differentiate Native code. The recommended approach is UNITY_ANDROID && !UNITY_EDITOR. If UNITY_EDITOR isn't filtered out, it can lead to errors during runtime.
4. It's advised to use the using statement for new instances of AndroidJavaClass and AndroidJavaObject. This practice ensures that after execution, Unity will automatically handle the corresponding code cleanup.
The other parts of the code are not elaborated on in this article. This pretty much covers the process of Unity Android development. If there's anything unclear, feel free to leave a comment.
For mobile gaming performance optimization, the Tencent WeTest platform offers the PerfDog tool, which provides detection of almost all relevant performance metrics. PerfDog empowers you with comprehensive performance metric analysis, ensuring that every aspect of your game's performance is finely tuned. Unlock the potential for efficient and precise testing, all geared towards enhancing player satisfaction.
Join us by registering for a trial of PerfDog today, and take your game optimization to the next level. Your players deserve the best – give them an unforgettable experience.