Android M Runtime Permission Issue in Unity Game Development

关于有史以来遇见过的最麻烦的游戏bug: Android M Permission

问题描述:

Android M 允许玩家在游戏安装后,修改游戏访问设备的权限。当玩家禁止游戏访问设备摄像头之后,游戏中的AR scene 依旧可以照常运行并不显示任何提示信息。

具体权限访问改变参考此处:https://blog.xamarin.com/requesting-runtime-permissions-in-android-marshmallow/

经过调查,游戏AR scene 的错误监控系统来自 Vufornia 的自带错误监控系统。然而,糟糕的是,Vufoirnia 并没有兼容 Android 的最新版本。每次AR scene 初始化时,访问的 Init 信息为 success。 这意味着,我必须自创一种检测方式,来检测游戏是否有权限访问设备的摄像头。

最开始,我尝试了两种方法:WebCamTexture (http://docs.unity3d.com/ScriptReference/WebCamTexture.html) 和 CameraDevice( https://developer.vuforia.com/library/sites/default/api/unity/classVuforia_1_1CameraDevice.html)。

前者是 Unity 提供的 api,后者是 Vuforinia 提供的对象。我本以为,如果摄像头访问权限受限后,api 返回的 active device 数目应该为 0。但是,实际返回对象数目依旧是 2 (前置和后置摄像头)。其次,我试图通过摄像头的各种参数,来区分权限访问情况。但是,无论是检测 Camera 的 buffer 是否在上一帧有更新,或者是检查 Camera 的 field of view,即使 permission 被禁止了,这些信息依旧可以被访问。

于是,我不得不把方向转向 Android M 的常规解决办法。http://jijiaxin89.com/2015/08/30/Android-s-Runtime-Permission/ 和 https://developer.android.com/training/permissions/requesting.html

这里的关键 api 是 checkSelfPermission。要调用该 api,最直接的办法是,创建自己的 jar,并添加到项目中。之后再用 C# 代码调用。但是,还有一种更简单的办法,即用 Unity 提供的 api 直接调用 Android 函数。方法在此:http://docs.unity3d.com/ScriptReference/AndroidJavaClass.html

我们既可以通过 AndrpidJavaClass 调用 static 函数,可以通过 AndroidJavaObject 调用 non-static 函数。以下是我的测试代码:

  1.             AndroidJavaClass jc = new AndroidJavaClass(“com.unity3d.player.UnityPlayer”);
  2.             AndroidJavaObject activity = jc.GetStatic<AndroidJavaObject>(“currentActivity”);
  3.             AndroidJavaObject context = activity.Call<AndroidJavaObject>(“getApplicationContext”);
  4.             AndroidJavaObject ContextCompat = new AndroidJavaObject(“android.support.v4.content.ContextCompat”);
  5.             AndroidJavaClass ActivityCompatClass = new AndroidJavaClass(“android.support.v4.app.ActivityCompat”);
  6.             int hasCameraPermission = context.Call<int>(“checkSelfPermission”, “android.permission.CAMERA” );
  7.             ActivityCompatClass.CallStatic(“requestPermissions”, context, “android.permission.CAMERA”, 1000 );
  8.             //int hasCameraPermission = ContextCompat.Call<int>(“checkSelfPermission”, context, “android.permission.CAMERA” );

但是糟糕的是,如果我用 ContextCompat.checkSelfPermission 调用 static 函数,程序将会报错显示无法找到该 static 函数。如果我使用 context.checkSelfPermission 调用 non-static 函数,虽然 method 会被调用,但是返回的值永远都是 0 (granted)。

后来经过再三检查,发现其问题在于我们使用的 library 不是最新版本,无法支持 Android api 23 及以上的版本。如果更新 lib 的话,会花费相当多的时间去清理和调整。

最后,我们尝试更改 Manifest 中的 targetVersion 来禁止用户修改游戏的访问权限。但是,这种方法也失败了。原因在于 targetVersion 的具体意思是指告知设备该程序已经在通过 targetVersion 的目标版本测试。然而我们的游戏实际并不满足目标版本,但是设备依然会按照应有的方式处理我们的程序。详细见此:

http://www.cnblogs.com/popapa/p/android_uses-sdk-element.html
http://developer.android.com/guide/topics/manifest/uses-sdk-element.html

最后是网上另外两名用户的解决办法: http://blog.trsquarelab.com/2015/11/  和 http://stackoverflow.com/questions/35027043/implementing-android-6-0-permissions-in-unity3d/

 

Error Handler

Usually in game development, we want each error to be detected immediately and processed gracefully. Recently I am working on error handler to handle all different error states in our game. Here I would like to talk about it in detailed.

To begin with, it should be clear that error detection and error process should be separated. In language like C++ and Java, they use try-catch to handle the error. But using try-catch might not be always a good idea. Joel Spolsky pointed out two drawbacks of using try-catch.

  1. They are invisible in the source code. Looking at a block of code, including functions which may or may not throw exceptions, there is no way to see which exceptions might be thrown and from where. This means that even careful code inspection doesn’t reveal potential bugs.
  2. They create too many possible exit points for a function. To write correct code, you really have to think about every possible code path through your function. Every time you call a function that can raise an exception and don’t catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state, or other code paths that you didn’t think about.

Moreover, for me, I would like to have my own error message data structure. Therefore, rather than use try-catch directly, I create my own error handler class. The general idea of this class is still similar to try-catch, it has two main functionality, which are error detection and error handle.

Here is the workflow when an error occurs:

  1. One error occurs
  2. All listeners handle this error event. If the error is solved, clear the error.
  3. Check whether error still exist
  4. If yes, bing player back to the first scene
  5. Show error message to player (UI pop up)
  6. Player could check whether error is solved by pressing error message UI button.

Back to my ErrorHandler class, it has following things:

Function

  • void ErrorStateReport();                                             //Call back function when an error is detected
  • bool CheckErrorState( ErrorType i_error);            //Check one certain type of error whether exists.
  • bool CheckError();                                                       //Check whether there is still an error
  • void ClearError();                                                        //Clear error, set errorType to None.
  • void SetError( ErrorType i_error);                         // Set errorType to a certain type of error.

Data Member

  • readonly Dictionary<ErrorType, string> ErrorMessageDictionary;                // Store display information for each error type
  • ErrorType ErrorType;                                                                                                 //Error type.
  • delegate void OnErrorState( string errorMessage, ErrorType errorType);     //Error delegate
  • event OnErrorStateEvent OnErrorState;                                                                //Error event

Here is the workflow by using ErrorHandler class:

Scene where error occurs

  • //Error occurs
  • ErrorHandler.ErrorStateReport( Error );                                                       //All listener would handle the error
  • if(ErrorClientHandler.CheckError()) ErrorClientHandler.ErrorType = // one error type
  • //Back to first scene

First scene

  • if(ErrorClientHandler.CheckError())  ShowErrorUI( ErrorClientHandler.ErrorMessageDictionary[ ErrorClientHandler.ErrorType], ErrorClientHandler.ErrorType);
  • //….we are in error state now
  • //If player press error message UI button
  • if( !ErrorClientHandler.CheckErrorState(ErrorClientHandler.ErrorType) ) //error solved
  • //Keep retrying, error still exists.

This is ErrorHandler class I create. And it could match the requirement of our game’s error handle perfectly.

 

New Update

I have to say game reset is also a very dangerous activity. It is ironic that when I tried to use error handler to reset the game, it would bring new errors and crash the whole game.

This is what resetGame function did before:

  • Uninit the game;
  • Game back to main menu scene;

But when game uninited itself, some monobehaviour scripts were still running, and they might want to have the access to game data. Unfortunately, game data just destroyed itself. Then new error (null exception) came out, and at last game crashed.

The solution to this problem is before uniniting the game data, make game first enter an empty game scene to ensure no object still has reference to game data. Then we uninit game data, and last bring player back to main menu scene. So the new workflow turns out to be:

  • Go to empty scene;
  • Uninit the game;
  • Game back to main menu scene;

Exceptions

Exceptions DoSomething()

Exceptions vs. status returns

About Unity asset memory leak issue

Loading and unloading asset could cause memory problem if we are not able to handle it carefully. Here you could find the problem we might meet, the solution and the reason why such a problem happens.

Example Problem

Texture assets remain in memory after scene changes

Description

When player is in scene A, those character textures that he has checked would be loaded, which is normal. But when he moves to scene B, those textures still remain in memory. To see memory profiler, go to Window -> Profiler, click memory panel, then chose detailed option. Run the game, click “Take Sampler:Editor”, then we could see all asset loaded in memory.

Solution:

The thing we need to do is set texture to null when dynamic card is destroyed (onDestroy()). Then call Resources.UnloadUnusedAssets() when the scene ends or changes as we use Resources.Load() to get the 2D texture.

Detailed Explanation

Usually we want to use Resources.UnloadUnusedAssets() to unload asset loaded from resource.And we have to make sure there is no reference to the asset itself before calling Resources.UnloadUnusedAssets(). In the example, the game object should be destroyed when scene changes.

However, Destroy() function does not work in a proper way. In detailed,  a lot objects in Unity, to be more precisely, all that are inherited from UnityEngine.Object, consists of two parts: a managed object and a native code c++ class in the engine itself. In the managed environment we only “see” the managed object that is “linked” with the native part. This is where two totally different worlds collide. When you call destroy on the managed object, Unity will destroy the native code part of the object. But the managed part of the class is still there! Unity will flag the class internally as “destroyed”.

In short, Unity uses a “trick” to make the managed part “pretend” to be null when the native part is destroyed. In the example, the character texture in dynamic card (2D texture derived from object) has not been destroyed correctly. There is still a reference to the texture asset loaded from Resources and Resources.UnloadUnusedAssets() could not unload it.

Therefore, in order to destroy the object properly, we have to set the asset to null, like it mentioned above. In this way, there is no reference to the texture asset loaded from Resources. And Resources.UnloadUnusedAssets() could work correctly at last.

 

Atlas Memory Problem

We probably meet the problem that atlas still remains in memory after scene changes. This is not what we want to see because usually the size of atlas is big. In order to solve that,we might need to do following things:

  • Call UIDrawCall.ReleaseInactive() or ReleaseAll()
  • Then call UnloadUnusedAssets().

Also, if the scene we work on has the static object, make sure object set to null before it is destroyed (including the texture, material and etc). Usually if we see ManagedStaticReference in “Reference by”, then we have to be careful with that.

Moreover, think twice before working on NGUI object, as NGUI object would have the reference to atlas. If the object is not destroyed correctly when scene changes, it could cause atlas remain in memory. 

Currently even Unity community could not provide available solution to atlas unloaded problem. And it is possible that NGUI or Unity itself still has some bugs. We have to rely on ourselves now.

 

Summary

To sum up, we need to check following issues if we meet similar memory problem:

1.Keep track of the asset loaded from resource, make sure it have been set to null when it is destroyed.

2.Be careful about the assignment operator. For example:

     Object obj = Resources.Load(“MyPrefab”);
GameObject instance = Instantiate(obj) as GameObject;
………
Destroy(instance);

      We also have to destroy obj if we want to call Resources.UnloadUnusedAssets() to unload the “MyPrefab” asset. This is why in 1 says “keep track of the loaded asset”. 

3.Make sure Resources.UnloadUnusedAssets() is called. Even though this function should be automatically called when scene changes ( I have tested it and it is true). It is better call Resources.UnloadUnusedAssets() explicitly just in case.

4.Make sure static object has been cleared up.

5. If the asset still could not be unloaded, try calling System.GC.Collect(). Although it might solve the problem, it is not recommended.

 

Reference

Does Destroy remove all instances?

Resources.UnloadUnusedAssets

Similar Texture Memory Problem

Unload NGUI atlas