效果展示:
开篇废话:
我现在所在的Team每周需要一个人给大家介绍一个知识点,或者新技术。这礼拜正好轮到我了,由于我工作才一年,面对那帮老鸟讲知识点感觉有点作死。所以我就准备选个新技术介绍一下。
由于我在大学里自学过一段时间Unity3D,所以我想介绍的技术就是它,但我现在做的是应用开发,不能做个小游戏去给大家演示。所以我想到比较简单,直观,而且有可能真正能用到的就是在Android应用中展示3D模型。比如在产品展示时直接把这个产品的3D模型展示出来而不是个图片,效果应该非常棒(OpenGL应该也可以做)。
思路定下以后就发现大学时学的Unity3D的内容基本忘光了,虽然偶尔有Unity3D的文章都会点开看看,但还是得重新学。记得当时学Unity3D的时候看过一个叫雨松MOMO的博客。那时年轻,懵懂,找不到方向的我还给雨松大神发了一封邮件去请教大学应该怎么学习和做游戏相关的问题,结果人家没回,导致我更加失落于是接着学android去了。。。又扯远了。。。于是我又找到他的博客,把Unity3D基础部分的相关文章都看了一遍。
但是他博客里有介绍如何在Unity3D中调用Android,而我想做的是在Android中调用Unity3D,而且是把Unity3D嵌套在ANDROID的视图里面。最后费了九牛二虎之力才把这个Demo做出来。
准备工作:
下面是我总结的流程,目的是使本文思路更加清晰一些:
1.Android端代码可以在Eclipse中开发(AndroidStudio没有试,应该也可以)
2.Unity3D端代码要在Unity中开发
3.Android和Unity3D端,两边都需要加入一些代码从而可以使之关联交互。
4.将Android端代码编译成jar文件以插件形式放入到Unity端中
5.在Unity中将整个项目Build成apk文件,然后安装到手机或模拟器里运行
本文主要讲解1,2,3。对于4,5建议大家去看雨松MOMO的Unity博客的第17篇和第18篇。
UnityPlay:
在编写Android端和Unity3d端代码前,有必要先了解一下可以使两部分交互的类UnityPlay。
个人理解UnityPlay是个Unity提供给外部交互的一个接口类。
为什么是“个人理解”?这我不得不爆粗口了,TMD官网根本就没有相关的API和文档(如果大家有谁找到一定给我来一份,就当我骂自己了)。
在关联Android时,想拿到UnityPlay以及相关类的jar包可以从下面的地址找到:Unity安装路径\Editor\Data\PlaybackEngines\androidplayer\bin在bin文件夹下有一个classes.jar的jar文件,它就是我们想要的。
而在bin同目录下有一个src文件,点击到最后有3个类,分别是UnityPlayerActivity.java,UnityPlayerProxyActivity.java,UnityPlayerNativeActivity.java。前两个打开个后只有一行代码,说的是UnityPlayerActivity和UnityPlayerProxyActivity都继承自UnityPlayerNativeActivity。而打开UnityPlayerNativeActivity中居然有代码,而且我估计这应该是UnityPlayerNativeActivity的源码。
由于关于UnityPlay的资料我只找到这么一个,所以我把UnityPlayerNativeActivity中的代码都贴出来,如果我注解有不对的地方希望大家指正。
-
-
-
-
-
- public class UnityPlayerNativeActivity extends NativeActivity
- {
-
- protected UnityPlayer mUnityPlayer;
-
- protected void onCreate (Bundle savedInstanceState)
- {
- requestWindowFeature(Window.FEATURE_NO_TITLE);
- super.onCreate(savedInstanceState);
-
- getWindow().takeSurface(null);
- setTheme(android.R.style.Theme_NoTitleBar_Fullscreen);
- getWindow().setFormat(PixelFormat.RGB_565);
-
-
- mUnityPlayer = new UnityPlayer(this);
-
- if (mUnityPlayer.getSettings ().getBoolean ("hide_status_bar", true))
- getWindow ().setFlags (WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
-
- int glesMode = mUnityPlayer.getSettings().getInt("gles_mode", 1);
- boolean trueColor8888 = false;
-
- mUnityPlayer.init(glesMode, trueColor8888);
-
-
- View playerView = mUnityPlayer.getView();
-
- setContentView(playerView);
-
- playerView.requestFocus();
- }
- protected void onDestroy ()
- {
-
- mUnityPlayer.quit();
- super.onDestroy();
- }
-
-
- protected void onPause()
- {
- super.onPause();
- mUnityPlayer.pause();
- }
- protected void onResume()
- {
- super.onResume();
- mUnityPlayer.resume();
- }
-
- public void onConfigurationChanged(Configuration newConfig)
- {
- super.onConfigurationChanged(newConfig);
- mUnityPlayer.configurationChanged(newConfig);
- }
- public void onWindowFocusChanged(boolean hasFocus)
- {
- super.onWindowFocusChanged(hasFocus);
- mUnityPlayer.windowFocusChanged(hasFocus);
- }
- public boolean dispatchKeyEvent(KeyEvent event)
- {
- if (event.getAction() == KeyEvent.ACTION_MULTIPLE)
- return mUnityPlayer.onKeyMultiple(event.getKeyCode(), event.getRepeatCount(), event);
- return super.dispatchKeyEvent(event);
- }
- }
看完这个类后就知道了为什么在自定义的Activity中继承了UnityPlayerActivity等类以后,只要重写了onCreate并调用super.onCreate()方法后不需要任何其他的代码就会自动的显示出Unity3D的视图。因为初始化Unity视图的代码都在UnityPlayerNativeActivity父类中实现了。
ANDROID端代码:
在写ANDROID代码的时候,一定要导入Unity3D提供给我们的jar包,jar包的位置我在上面说了。引入jar包加入到buildpath中这些最基本的我就不多说了。
要想和Unity交互,我们就不能继承ANDROID提供给我们的Activity,我们需要继承刚才jar包中引入的Unity提供的Activity类,一共有这么3个:
UnityPlayerActivity,UnityPlayerProxyActivity,UnityPlayerNativeActivity。具体区别不知道,因为没有文档,没有API,没有源码(这里再次鄙视一下)。刚才我们看过UnityPlayerNativeActivity的代码(虽然很短,但我觉得这个就是源码),知道UnityPlayerActivity,UnityPlayerProxyActivity都是它的子类,而且最终父类为NativeActivity。所以我们继承Unity提供的最外层的子类是最好的选择,我这里选择的是UnityPlayerActivity,因为名字最简单,觉得该封装的都应该封装好了。
- public class MainActivity extends UnityPlayerActivity {
-
- private Button topButton;
- private Button bottomButton;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
-
- setContentView(R.layout.test);
-
-
- View playerView = mUnityPlayer.getView();
-
- LinearLayout ll = (LinearLayout) findViewById(R.id.unityViewLyaout);
- ll.addView(playerView);
-
-
- topButton = (Button) findViewById(R.id.topButton);
- topButton.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(View v) {
-
-
- UnityPlayer.UnitySendMessage("Main Camera","Previous","");
- }
- });
-
-
- bottomButton = (Button) findViewById(R.id.bottomBtn);
- bottomButton.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(View v) {
-
- UnityPlayer.UnitySendMessage("Main Camera","Next","");
- }
- });
- }
- }
最后看一下Android端的布局文件,布局很简单,上下各有一个button按钮,两个按钮中间是Unity的视图。
- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" >
-
- <Button
- android:id="@+id/topButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:text="PREVIOUS" />
-
- <LinearLayout
- android:id="@+id/unityViewLyaout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_above="@+id/bottomBtn"
- android:layout_below="@+id/topButton"
- android:orientation="horizontal" >
- </LinearLayout>
-
- <Button
- android:id="@+id/bottomBtn"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentBottom="true"
- android:text="NEXT" />
-
- </RelativeLayout>
Android端的代码就介绍完了,很简单。唯一的难点就是UnityPlayerActivity和UnityPlayer的使用,就这两个破玩意花了我好几天的时间,很简单的东西不知道为什么官方不给个文档或者API(也可能我太挫没找到。。。)
Unity3D端代码:
先看一下我的项目结构:
JavaScript存放的是脚本
Models存放的是我在Assert Store中下载的免费的一些模型文件
Plugins下是我的Android工程,具体做法参考网上教程(这里推荐雨松大神的第17篇)
Prefab我是调整模型后定义的预制体
在场景中,我只有一个摄像机,和一个直射光。将脚本绑定到摄像机上,然后将之前调整好的5个预设模型添加到脚本的相应对象中。
下面是脚本的代码,关于模型的旋转缩放是直接用了雨松MOMO的一篇文章中的代码,然后再加上了本例中的一些逻辑而组成的。
- #pragma strict
-
-
- var car : GameObject;
- var helicopter : GameObject;
- var suv : GameObject;
- var plane : GameObject;
- var tank : GameObject;
-
-
- private var index : int;
-
- private var models : GameObject[];
-
- private var mCurrentGameObject : GameObject;
-
-
-
-
-
-
- var distance = 10.0;
-
- var xSpeed = 250.0;
- var ySpeed = 120.0;
-
- var yMinLimit = -20;
- var yMaxLimit = 80;
-
- var x = 0.0;
- var y = 0.0;
-
- private var oldPosition1 : Vector2;
- private var oldPosition2 : Vector2;
-
-
- function Start () {
-
- index = 0;
- models = new GameObject[5];
- models[0] = car;
- models[1] = helicopter;
- models[2] = suv;
- models[3] = plane;
- models[4] = tank;
-
- mCurrentGameObject = Instantiate(models[index], Vector3(0,0,0), Quaternion.Euler(-20,0,0));
-
-
- var angles = transform.eulerAngles;
- x = angles.y;
- y = angles.x;
- }
-
- function Update () {
-
-
- if(Input.touchCount == 1)
- {
-
- if(Input.GetTouch(0).phase==TouchPhase.Moved)
- {
-
- x += Input.GetAxis("Mouse X") * xSpeed * 0.02;
- y -= Input.GetAxis("Mouse Y") * ySpeed * 0.02;
-
- }
- }
-
-
- if(Input.touchCount > 1 )
- {
-
- if(Input.GetTouch(0).phase==TouchPhase.Moved||Input.GetTouch(1).phase==TouchPhase.Moved)
- {
-
- var tempPosition1 = Input.GetTouch(0).position;
- var tempPosition2 = Input.GetTouch(1).position;
-
- if(isEnlarge(oldPosition1,oldPosition2,tempPosition1,tempPosition2))
- {
-
-
- if(distance > 3)
- {
- distance -= 0.5;
- }
- }else
- {
-
-
- if(distance < 18.5)
- {
- distance += 0.5;
- }
- }
-
- oldPosition1=tempPosition1;
- oldPosition2=tempPosition2;
- }
- }
- }
-
-
- function isEnlarge(oP1 : Vector2,oP2 : Vector2,nP1 : Vector2,nP2 : Vector2) : boolean
- {
-
- var leng1 =Mathf.Sqrt((oP1.x-oP2.x)*(oP1.x-oP2.x)+(oP1.y-oP2.y)*(oP1.y-oP2.y));
- var leng2 =Mathf.Sqrt((nP1.x-nP2.x)*(nP1.x-nP2.x)+(nP1.y-nP2.y)*(nP1.y-nP2.y));
- if(leng1 < leng2)
- {
-
- return true;
- }else
- {
-
- return false;
- }
- }
-
-
- function LateUpdate () {
-
-
- if (mCurrentGameObject.transform) {
-
-
- y = ClampAngle(y, yMinLimit, yMaxLimit);
- var rotation = Quaternion.Euler(y, x, 0);
- var position = rotation * Vector3(0.0, 0.0, -distance) + mCurrentGameObject.transform.position;
-
- transform.rotation = rotation;
- transform.position = position;
- }
- }
-
- static function ClampAngle (angle : float, min : float, max : float) {
- if (angle < -360)
- angle += 360;
- if (angle > 360)
- angle -= 360;
- return Mathf.Clamp (angle, min, max);
- }
-
-
- function Next () {
- index = index+1;
- if (index > models.Length-1) {
- index = 0;
- }
- Debug.Log("next");
-
- Destroy(mCurrentGameObject);
-
- mCurrentGameObject = Instantiate(models[index]);
- }
-
-
- function Previous () {
- index = index-1;
- if (index < 0) {
- index = models.Length-1;
- }
- Debug.Log("previous");
-
- Destroy(mCurrentGameObject);
-
- mCurrentGameObject = Instantiate(models[index]);
- }
最后就是在Unity3D中将工程Build成APK文件,然后再手机或模拟器中运行(如果手机或模拟器连着Eclipse则可以打出log方便调试找错)。
最后附上代码Demo:
Unity端代码太大了,所以我就把Android端和Unity端代码上传到百度云了。而apk文件上传到csdn,如果只想看效果的可以下载来试试。
代码点击下载