Unity Android Plugin 开发指南

本文将介绍如何在Unity工程中使用Android或者Java的库,包括:

  • 如何在Unity项目中使用Android Plugin

  • Unity-Android相互调用

  • Unity接口设计的最佳实践

  • 如何构建Unity-Android混合项目

  • 如何调试Unity和Android代码

  • 附录:跨虚拟机调用的实现

如何在Unity项目中使用Android Plugin

Android Plugin需要包含一个jar和对应的封装代码。后者用来封装Android代码,提供给Unity项目使用。

jar放在Unity项目的 /Assets/Plugins/Android 中,Android插件的其他依赖也放在此处。

封装代码可以是C#文件,或者dll文件,都放在 /Assets 中,若是dll,需在Unity C#工程中添加此dll依赖。

此外,如果Android插件包含资源,按照原有目录结构放到 /Assets/Plugins/Android 中即可。

如果需要额外的系统权限,需要在AndroidManifest.xml中添加,这个文件默认是没有的,如果要修改的话,必须手动添加一份Unity可用的manifest,可参考【附录】中提供的模板。

最后,工程的结构类似这样:

Assets
└── Plugins
    ├── Android
    │   ├── AndroidManifest.xml
    │   ├── android_sdk.jar
    │   └── res
    │       ├── values
    │       └── drawable
    └── unity_wrapper.dll

Unity 从5.2.0b3版本开始很好地支持aar格式的文件,可以将资源打包进aar中,不必再放置到该目录下

Unity与Android之间相互调用

准确来说,应该是两个VM之间的相互调用:mono/il2cpp 和 dalvik/art,分别运行Unity应用和Android应用,这两个虚拟机运行在同一个进程中。

为了方便起见,后文将前者称为Unity,后者称为Android

如上图所示,Unity通过 UnityEngine 提供的API调用Android的方法;Android借助 com.unity.player 包提供的API调用Unity的方法。

前者可以直接调用Android对象或者类的方法,而后者只能调用Unity中指定 GameObject 所挂载的脚本的方法,或者通过动态代理的方式调用Unity的方法。

Unity调用Java方法

UnityEngine提供了两个类来分别访问Java的实例对象以及类对象:

AndroidJavaObjectAndroidJavaClass

前者表示 java.lang.Object 或其子类,后者表示 java.lang.Class 。他们提供相同的实例方法:

方法返回值说明
Call void调用实例方法
Call<T> T调用实例方法
CallStatic void调用类方法
CallStatic<T> T调用实例方法
Get<T> T获取成员变量
GetStatic<T> T获取类的成员变量
Set(T) void设置成员变量
SetStatic(T) void设置类的成员变量

注意:

  • T的类型只能为原始值类型(int、long、string等等),或者 AndroidJavaObjectAndroidJavaClass ,或者内容为原始值类型或 AndroidJavaObject 的数组

  • Get和Set方法直接操作成员变量,而不是通过getter或setter

下面将通过一段代码来演示:如何获取一个 AndroidJavaClass 实例,并且调用其 getInstance 方法获取其对象,然后调用此对象的方法。

在开始之前,先看一下我们用到的Java类

package example;

public class Player {
  private final static Player instance = new Player();
  public static Player getInstance() {
    return instance;
  }

  public float volume = 1.0f;
  public int getDuration() {}
  public void setDataSource(String dataSource) {}
  public AudioInfomation getAudioInfomation() {}
}

首先,在工程中添加 UnityEngine.dll 依赖,该文件位于Unity安装目录下的 Editor/Data/Managed 目录中,注意,添加依赖后,将其设置为不拷贝到本地。

现在,我们来获取 Player 这个类并获取其单例:

AndroidJavaObject player = new AndroidJavaClass("example.Player").CallStatic<AndroidJavaObject>("getInstance");

然后对 player 对象调用其Java方法:

player.Set("volume", 0.8f);
player.Call("setDataSource", "http://example.com/stream.m4a");
int duration = player.Call<int>("getDuration");
AndroidJavaObject info = player.Call<AndroidJavaObject>("getAudioInfomation");

注意,返回值类型为 AndroidJavaObject 的方法有个共同的缺陷:如果Android侧返回null,该方法将报错: JNI: Init'd AndroidJavaObject with null ptr!

这是因为,在 AndroidJavaObject 的构造函数中,遇到 IntPtr.Zero(即null) 会报错:

internal AndroidJavaObject(IntPtr jobject) : this(){
  if (jobject == IntPtr.Zero){
    throw new Exception("JNI: Init'd AndroidJavaObject with null ptr!");
  }
  // ...
}

该缺陷存在于5.3.1f1版本之前的UnityEngine,一个可行的办法是:先获取Android方法返回结果的指针,如果指针为空就返回null,否则返回指针的对象。

Android调用Unity方法

在Android中,有两种方式调用Unity的方法:

  • 通过 AndroidJavaProxy 进行无感知调用

  • 通过 com.unity3d.player.UnityPlayer.UnitySendMessage 方法显式调用

AndroidJavaProxy

AndroidJavaProxy 常用于在Unity中实现Java的interface,比如有这么一个java interface:

package demo;

interface PlayStateListener {
  void onBufferFinished(SongInfo songInfo);
  void onBufferProgress(String songId, long buffered, long total);
}

对应的C#类就是这样:

class PlayStateChangedHandler : AndroidJavaProxy {
  internal PlayStateChangedHandler() : base(new AndroidJavaClass("demo.PlayStateListener")) {}
  public void onBufferFinished(AndroidJavaObject songInfoObject) {}
  public void onBufferProgress(string songId, long buffered, long total) {}
}

有几点需要注意:

  1. Unity侧的方法必须为public,且有相同的名称和类似的签名

  2. 如果Android侧方法的传参或返回值为类类型,对应Unity侧只能为 AndroidJavaObject

  3. 4.6.8f1版本的UnityEngine有BUG,无法在 AndroidJavaProxy 中传递long类型的值,该问题在Unity 5中已经修复

有关 AndroidJavaProxy 的实现,在附录中有详细介绍

UnityPlayer.UnitySendMessage

这需在Android工程中添加Unity提供的jar依赖,它位于Unity安装目录下:

/Editor/Data/PlaybackEngines/AndroidPlayer/Viariations/{backend}/{buildType}/Classes/classes.jar

其中,backend是Unity项目脚本执行器的类型,有mono和il2cpp两种,与Unity项目的”Script Backend”一致。

然后通过以下代码来访问挂载在TGameObj对象上的脚本的OnButtonClick方法:

UnityPlayer.UnitySendMessage(&quot;TGameObj&quot;, &quot;OnButtonClick&quot;, &quot;Greetings from Java&quot;);

Unity接口设计的最佳实践

本节将介绍一个用于封装Java代码的通用设计方式,可以高效地将Java代码的API“移植”到C#,同时保持可扩展性。该设计将Java代码中的类及其结构反射到C#代码中,至于该类的细节(比如继承关系、接口实现等)将被忽略,因为需要反射的都是暴露给用户的API接口,用户不应该关心这些细节。

如下图所示:

Java中的 demo.Foo 类通过 Reflection 反射到C#的 Mirrored.Foo 中, demo.Foo 中的公共字段和方法都按照原有结构被反射。

注意,这里的反射只是单向地从Java反射到C#。如果要从C#反射到Java,可以参考本节进行扩展。

反射的实现

在开始之前,我们需要明确哪些类需要反射。对于int, long, double等原始类型以及string类型,UnityEngine已经帮我们处理好了,只剩下 java.lang.Object 的派生类需要我们反射。

反射基类的设计

我们使用 AndroidObjectMirror 作为反射类的父类。

public abstract class AndroidObjectMirror : IDisposable {
  protected AndroidJavaObject AJObject { get; private set; }

  internal void FromJava(AndroidJavaObject ajo) {
      AJObject = ajo;
      InitFromJava(ajo);
  }

  public virtual void Dispose() { AJObject?.Dispose(); }

  protected virtual InitFromJava(AndroidJavaObject ajo) {}
}

AJObject 这个反射对象被创建时,被反射对象的引用计数将会增加( AndroidJNISafe.NewGlobalRef ),在 Dispose 方法中,其引用计数将会减少( AndroidJNISafe.DeleteGlobalRef )。

之后,子类通过覆写 InitFromJava 方法来进行成员变量的初始化:

子类可以创建和被反射类“一样的”方法,并将所有的调用委托给成员变量 AJObject 即可。例如:

int Add(int a, int b) {
  return AJObject.Call<int>("add", a, b);
}

总结一下,反射的逻辑如下图所示:

反射的实现

借助于 AndroidObjectMirror ,我们可以这样来定义上文提及的 example.Player 的反射类:

class Player : AndroidObjectMirror {
  public static Player Instance {
    get {
      var javaObject = AJObject.CallStatic<AndroidJavaObject>("getInstance");
      return Reflection.Reflect<Player>(javaObject);
    }
  }

  public float Volume {
    get { return AJObject.Get<float>("volume"); }
    set { AJObject.Set<float>("volume", value); }
  }

  public void Start() { AJObject.Call("start"); }
  // ..
}

注意,在获取单例时,我们用了这样一行代码:

return Reflection.Reflect (javaObject);

Reflection 这个工具类用来反射Java的对象,即将 AndroidJavaObject 的对象反射为派生自 AndroidObjectMirror 的类的对象。

其中的 Reflect 方法是这样实现的:

public static T Reflect<T>(AndroidJavaObject ajo) where T : AndroidObjectMirror, new() {
    if (ajo == null) {
        return default(T);
    }
    try {
        var result = new T();
        result.FromJava(ajo);
        return result;
    }
    catch (Exception e) {
        Debug.LogError("failed to reflect from Java : " + e.Message);
        return default(T);
    }
}

这里的逻辑很直接:创建一个 AndroidJavaObject 对象 ajo ,然后在 InitFromJava 方法中通过 ajo 来初始化这个对象的成员变量。注意,这里约束了类型 T 必须提供无参公共构造函数,因此 AndroidJavaMirror 必须通过 InitFromJava(AndroidJavaObject) 来初始化,而没有将 AndroidJavaObject 放在构建函数中。

至于 InitFromJava 方法,它可以是这样:

protected override void InitFromJava(AndroidJavaObject ajo) {
    title = ajo.Get<string>("title");
}

或者类似反序列化的方式在运行时进行解析:

protected virtual void InitFromJava(AndroidJavaObject androidJavaObject) {
  var namingConverter = DefaultNamingConverter.Instance;
  var publicFields = GetType().GetFields();
  foreach (var field in publicFields) {
    var javaFieldName = namingConverter.GetJavaFieldName(field.Name);
    var value = androidJavaObject.Get(field.GetType(), javaFieldName);
    field.SetValue(this, value, BindingFlags.SetField, null, null);
  }
}

如何构建Unity-Android混合项目

本节将介绍如何使用Gradle来构建混合了不同平台项目的工程。

以一个SDK类型的工程为例,我们来看一下工程的内容:

  1. Android SDK

  2. Android Demo (快速调试)

  3. Unity Bridge (封装Android SDK)

  4. Unity Demo (演示并调试Unity Bridge)

目录结构如下:

RootDir (工程根目录)
|
|-- Android (Android相关模块)
|   |-- Demo_Android
|   |-- SDK_Android
|
|-- Unity (Unity相关模块)
    |-- Demo_Unity
    |-- Bridge_Unity

其中:

  • Android的两个模块可以用Android gradle插件进行编译与打包

  • Bridge_Unity 可以用msbuild(windows)或者xbuild(linux)构建

  • Demo_Unity 需要购买了Unity Pro之后才能自动化构建。

接下来,我们将在各自模块的构建脚本中添加构建任务,分别构建这些模块,最后,在工程的根构建脚本中,创建自动化的构建脚本。

Android SDK的构建

Jar包构建任务

SDK将以Jar的形式提供给Unity Bridge使用,因此需要添加打包成jar的构建任务。我们利用已有的Android构建任务链,创建Jar构建任务。

已有的构建脚本位于 RootDir/Android/SDK_Android/build.gradle ,在其中加入Jar构建任务:

android.libraryVariants.all { v ->
  def type = v.name.capitalize()
  task("jar$type", type: Jar) {
      archiveName "sdk-$type.jar"
      dependsOn v.javaCompile
      from v.javaCompile.destinationDir, configurations.compile.collect {
          it.isDirectory() ? it : zipTree(it)
      }
  }
}

task 后面的闭包会在gradle脚本构建时运行,用来定义此任务的属性:

  • archiveName: 输出Jar包的文件名,默认为模块名称

  • dependsOn: 此任务的依赖

  • from: 要打包的class

这里需要注意:

依赖

dependsOn: v.javaCompile

此任务必须在 v.javaCompile 完成之后运行,即java文件被编译成class文件之后再将这些class打包成Jar。

要打包的class
from v.javaCompile.destinationDir, configurations.compile.collect {
  it.isDirectory() ? it : zipTree(it)
}

这里说明要打包的class有两处:模块自身的和依赖的Jar包。

Proguard构建任务

对外发布时,通常需要对代码进行混淆。对于我们自定义的Jar任务,必须手动添加混淆任务:

task("proguardJar$type", type: ProGuardTask) {
    dependsOn "jar$type"
    configuration android.getDefaultProguardFile('proguard-android.txt')
    configuration 'proguard-rules.pro'
    injars "build/libs/sdk-$type.jar"
    outjars "$outputFolder</span>/sdk-<span class="hljs-variable">$suffix.jar"
    dontshrink
    ignorewarnings
}.doFirst {
    delete outputFolder
}

注意其中的 dependsOn "jar$type" ,这样就将混淆任务和jar任务串联了起来。

发布任务

为了便于其他构件脚本获取此模块的最新构建结果,我们将输出的Jar拷贝到 latest 目录中。

task("buildJar$type", type: Copy, group: 'build') {
    dependsOn "cleanBuildJar$type"
    from outputFolder
    into "build/outputs/libs/latest/$type
    if (type.equals("Release")) {
        dependsOn "proguardJar$type"
    } else {
        dependsOn "jar$type"
    }
}

至此,Android SDK构建任务添加完成。

Android Demo的构建

Andriod Gradle Plugin已经提供Demo的构建任务。

Unity Bridge的构建

在开始之前,我们需要配置好构建环境:对于Windows系统,需要用到msbuild,它会随着Visutal Studio一同安装;对于linux/unix系统,可以使用xbuild,它是 Mono 里面的一个工具。下文将使用xbuild。

准备工作完成后,我们来创建 CSharpBuildTask 这个构建任务。该任务其实就是封装了对xbuild的调用:

package demo

class CSharpBuildTask extends DefaultTask {
    @Input File solutionFile;
    @Input String configuration;
    @Input String builderCmd = "/usr/local/bin/xbuild"

    @TaskAction def compile() {
        def cmd = "$builderCmd $solutionFile"
        if (configuration != null) {
            cmd += " /p:Configuration=$configuration"
        }
        def proc = cmd.execute()
        proc.waitFor()
        if (proc.exitValue() != 0) {
            throw new BuildException("failed to build csharp project: ${proc.getErrorStream()}", null)
        }
    }
}

CSharpBuildTask.groovy 放在 RootDir/buildSrc/src/main/groovy/demo 下,这样就可以在所有子模块中使用该任务。

之后,在 RootDir/Unity/Bridge_Unity 目录下创建 build.gradle 文件,作为此模块的构建脚本。内容为:

import demo.CSharpBuildTask

def buildTypes = ["Release", "Debug"]
def localProps = new Properties()
localProps.load(project.file('local.properties').newDataInputStream())
buildTypes.each { v ->
    task("buildLib$v", type: CSharpBuildTask) {
        builderCmd = localProps["msbuild.dir"].toString()
        solutionFile = new File("Unity_Bridge")
        configuration = v
    }
}

local.properties 文件位于 RootDir/Unity/Bridge_Unity ,内容为:

# local.properties
msbuild.dir=/usr/local/bin/xbuild

至此,我们用gradle查看一下是否成功创建了此构建任务:

$ gradlew tasks

可以看到, buildLib 构建任务已经创建。

Unity Demo的构建

受限于Unity,只有Unity Pro及以上版本才支持代码或者命令行的方式进行构建。

首先,我们需要在 /Asset/Editor 中创建一个脚本,通过BuildPipeLine来构建Unity工程:

public class BuildScript: MonoBehaviour{
  static void BuildAndroid(){
    string[] scenes = {"Assets/Demo.unity"};
    BuildPipeline.BuildPlayer(scenes, "AndroidBuild", BuildTarget.Android, BuildOptions.None);
  }
}

然后,在 RootDir/buildSrc 中创建 UnityBuildTask :

class UnityBuildTask extends DefaultTask {
    @Input String unityExePath

    @TaskAction def compile() {
        def cmd = "$unityExePath -quit -batchmode -executeMethod BuildScript.BuildAndroid"
        def proc = cmd.execute()
        proc.waitFor()
        if (proc.exitValue() != 0) {
            throw new BuildException("failed to build unity project", null)
        }
    }
}

最后,在 RootDir/Unity/Demo_Unity 中创建 build.gradle ,并在其中创建一个构建任务:

// 有关localProps见前文
task("buildUnityDemo"type: UnityBuildTask, group: 'build') {
  unityExePath = localProps["unitybuild.dir"].toString();
}

BuildPipeLine以及Unity的命令行调用可以参考官方文档: http://docs.unity3d.com/Manual/CommandLineArguments.html

混合构建

上面已经介绍了各个模块各自的构建方法,现在,我们将在根模块的构建脚本中将他们串联起来。

首先来梳理一下所有构建任务之间的依赖关系,已有的构建任务有:

其中,箭头表示依赖关系,Unity的Demo同时依赖于Unity和Android的SDK,同时还要将生成的SDK拷贝到Unity Demo项目中的特定位置,这样Demo才能正常运行。

这些构建任务的依赖关系如下图所示:

我们在根模块中创建这些构建任务:

  • copyUnitySDKToDemo:将生成的Unity SDK拷贝到Unity Demo

  • copyAndroidSDKToDemo:将生成的Android SDK拷贝到Unity Demo

  • buildUnitySDK:buildLib的马甲

  • buildAndroidSDK:buildJar的马甲

  • buildUnityDemo:构建Unity demo

  • buildAndroidDemo: 构建Android demo

我们可以在根模块的 build.gradle 中添加这些任务,但会使得 build.gradle 变得非常混乱。因此我们采用Plugin的方式,来进行这些任务的创建。

现在我们来创建 SDKBuildPlugin ,在 RootDir/buildSrc/src/main/groovy/demo 中新建 SDKBuildPlugin.groovy :

package demo

class SDKBuildPlugin implements Plugin<Project> {
  def buildTypes = ["Release", "Debug"]
  @Override
  void apply(Project project) {
  }
}

接下来,为每个Build Type创建构建任务。在apply方法中,添加如下代码:

buildTypes.each { v ->
  project.task("buildAndroidSDK$v",
    dependsOn: ":sdk_android:buildJar$v")    
  project.task("buildUnityDemo$v") {
    dependsOn "cleanUnityDemo", "copyUnitySDKToDemo$v"</span>, <span class="hljs-string">"copyAndroidSDKToDemo$v"
  }
  project.task("copyAndroidSDKToDemo$v", type: Copy) {
    dependsOn "buildAndroidSDK$v"
    from "$androidSDKProjectDir/build/outputs/libs/latest/$v/"
    into "$unityDemoProjectDir/Assets/Plugins/Android"
          include "*.jar"
    }
  // ...
}

这里创建了三个典型的任务,其中 buildAndroidSDK 仅声明其依赖于 sdk_android 模块的 buildJar 任务,相当于为 buildJar 任务创建了一个别名。

其他的构建任务的创建不做赘述。

最后在 build.gradle 中应用此插件:

// build.gradle
import com.tencent.qqmusic.MusicUnitySDKBuildPlugin
// 中间略
apply plugin: MusicUnitySDKBuildPlugin

SDK的发布任务

SDK对外提供的内容比较繁杂,包括:

  • SDK的库文件(dll与jar)

  • Demo APP或工程

  • Demo 工程

  • 接口文档

  • Change log

这些内容都可以通过gradle的构建任务来自动完成。发布目录的结构如下:

Publish
|-- 1.0
|   |-- 1.0.0.0
|   |   |-- Debug
|   |   |-- Release
|   |   |-- ChangeLog.md
|   |   |-- 接口文档.md
|   |   |-- Demo
|   |
|   |-- 1.0.0.1
|
|-- 2.0
.   
.

构建任务的结构如下图:

这里的构建任务都很简单,不做详述。注意拷贝Demo工程的时候,需要过滤掉build结果。

至此,我们完成了SDK的构建系统。

如何调试

C#和Java的调试都只能通过adb远程调试来进行。

首先用USB连接手机,在命令行中输入 adb tcpip 5555

然后进入adb shell,用ifconfig查看手机的ip地址,之后通过 adb connect xxx.xxx.xxx.xxx:5555 连接手机

连接成功之后就可以通过MonoDevelop或者Android Studio的【Attach to process】进行调试了。

注意:

如果使用Xamarian进行C#代码的调试,可能无法找到【Attach to process】,这时候需要下载这个插件:

http://forum.unity3d.com/threads/unity-add-ins-for-monodevelop-xamarin-studio-5-9.329880/

如果在Android Studio中无法看到程序的进程,请确保包含Java代码的Android工程已经被正确载入

附录

AndroidJavaObject.Call的实现

这里分C#和Java两部分讲解。

C#部分

整个调用序列如下图:

简单来说,整个流程为:

  1. 通过 GetMethodId 找到方法对应的内存地址

  2. 创建入参,同时处理 AndroidJavaObjectAndroidJavaProxy 等特殊类型的参数

  3. 通过内存地址调用目标方法

其中,最关键的部分在于 1.1.1 AndroidJNI.CallStaticObjectMethod ,这个方法用于调用Android侧对象或者类的方法,其中:

  • ReflectionHelper_classPtr : 指向Java类 com.unity.player.ReflectionHelper 的指针

  • getMethodId_ptr : 指向上述Java类的 getMethodID 方法的指针

  • methodInfo : 包括方法名、签名、是否静态方法等信息

意思就是,调用Android侧的 ReflectionHelper.getMethodID 方法,先在Android侧获取到 methodInfo 描述的 Method 实例,然后将其指针传回给Unity侧。

Java部分

这部分主要是 ReflectionHelper 这个类,负责获取Android侧类的成员(变量、方法、构造函数),以及创建用于 AndroidJavaProxy 的Android侧proxy对象。

AndroidJavaProxy的实现

首先,我们来看一下如何从 AndroidJavaProxy 生成一个 java.lang.Proxy

在上一节中,我们知道,所有的 AndroidJavaObject.Call 方法都会调用 AndroidJNIHelper.CreateJNIArgArray 方法,该方法就由 AndroidJavaProxy 实例生成了一个 Proxy 实例:

class _AndroidJNIHelper {
  public static jvalue[] CreateJNIArgArray(object[] args) {
    // ...
    else if (obj is AndroidJavaProxy) {
      array[num].l = AndroidJNIHelper.CreateJavaProxy((AndroidJavaProxy)obj);
    }
    // ...
  }
}

虽然 AndroidJNIHelper.CreateJavaProxy(AndroidJavaProxy) 这个方法是native的,无法分析其实现,但是我们可以参考 _AndroidJNIHelper.CreateJavaProxy(int,AndroidJavaProxy) 方法:

// _AndroidJNIHelper
public static IntPtr CreateJavaProxy(int delegateHandle, AndroidJavaProxy proxy){
  return AndroidReflection.NewProxyInstance(delegateHandle, proxy.javaInterface.GetRawClass());
}

两者的实现应该是类似的,最终都是调用Android侧的 ReflectionHelper.newProxyInstance 方法,用来在Android侧创建一个动态代理:

// ReflectionHelper
protected static Object newProxyInstance(int paramInt, final Class[] paramArrayOfClass){
  // ..
  return Proxy.newProxyInstance(ReflectionHelper.class.getClassLoader(), paramArrayOfClass, new InvocationHandler()
  {
    public final Object invoke(Object proxy, Method method, Object[] args)
    {
      return ReflectionHelper.nativeProxyInvoke(this.a, method.getName(), args);
    }
   // ..
  });
}

可以看到,Android侧通过 ReflectionHelper.nativeProxyInvoke 将该侧的方法调用代理到了Unity侧,而Unity侧对应的方法为:

// UnityEngine._AndroidJNIHelper
public static IntPtr InvokeJavaProxyMethod(AndroidJavaProxy proxy, IntPtr jmethodName, IntPtr jargs){
  // ...
  IntPtr result;
  using (AndroidJavaObject androidJavaObject = proxy.Invoke(AndroidJNI.GetStringUTFChars(jmethodName), array)){
    if (androidJavaObject == null) {
      result = IntPtr.Zero;
    } else {
      result = AndroidJNI.NewLocalRef(androidJavaObject.GetRawObject());
    }
  }
  return result;
}

最终通过 proxy.Invoke 调用自己的目标方法。

AndroidManifest.xml

见 https://github.com/yhd4711499/unity_android_plugin/blob/master/AndroidManifest.xml

其中的 @string/app_name@drawable/app_icon 为Unity项目中包含的资源,与Android项目中的资源无关。

如果同时还用到了自己的Activity,需要将其中的 <activity android:name=""/> 改成自己的。

转载请注明原作者:haodongyuan@tencent.com

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值