Skip to content

mdklenk/UnityTest

Repository files navigation

Running Unity in an android.app.Fragment Demo

The purpose of this project is to demonstrate how to create an instance of UnityPlayer (the compiled code and scene from Unity) that is executed and run concurrently with native Android Java code. This is done to accelerate switching between the Unity view and the native Android application view. There are multiple methods to achieve this, the "Ultimate" branch handles it via embedding of the UnityPlayer view and the Android application in separate fragments, whereas the implementation of the "Master" brand uses the even simpler way of constraining the UnityPlayer to a specific android.view.View Object and toggling its visibility.

The UnityPlayer itself is handled as a sort of singleton within the UnityPlayerActivity (the class that is called by the Android OS launcher, as specified in the AndroidManifest.xml). TODO here is to extract UnityPlayerActivity to its own proper singleton and update the references in the other files.

Getting Started

The repository contains two branches which each contain a slightly different implementation of the demonstration described above. Both repository branches can be imported and compiled within an Android development environment, the required Unity3d scene is included as .jar archive.

Prerequisites

I have successfully compiled this on Android Studio by IntelliJ and JDK 1.8, not just Java Runtime Environment JRE, to run it you need an Android Device running at least Android 4.2 or an equivalent emulator.

Installing

First download and unpack repository (as .zip or clone it). In Android Studio (Eclipse et al. should be equivalent) go to "File -> New -> Import Project" and point it towards the folder containing the unpacked repository. This will create a new project that is ready to compile.

Adding your own Unity Project

Simply export the Unity Project for Android. Import the project into Android Studio ("File-> New -> Import Project"), copy the .java and the /res/layout files from UnityTest repository into the newly exported project (overwriting the existing UnityPlayerActivity). Then compile and run. To switch back to Android you must call the following method from Unity:

  public void callMeNonStatic(String s){
        Log.d("Non-Static", "Non-Static Call from Unity at " + s);
        getFragmentManager().beginTransaction().hide(unityFragment).show(androidButtonFragment).commit();
  }

This can be done in Unity like so, for example:

    var androidJC = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    var javaObject = androidJC.GetStatic<AndroidJavaObject>("currentActivity");
    javaObject.CallStatic("callMeStatic", "Call to Android");
    javaObject.Call("callMeNonStatic", "Call to Android");

Running

The rest should be straightforward compilation and deployment to run it and see how fast it switches between Unity and Android (once Unity is started for the first time).

Detailed Description

Now for a detailed play-by-play of what is going on when the application is run. The Android OS calls UnityPlayerActivity, as specified in the AndroidManifest.xml that I took from the one automatically generated by Unity's Android export feature.

UnityPlayerActivity

UnityPlayerActivity is a straightforward android.app.Activity. This class manages the Fragments by instantiating, running and switching them when so instructed. It also is the class that is being called by the C# script (see file in Repository for details) and the class that UnityPlayer resides in (see TODO above, this could be extracted to its own class for better organization).

Member fields: // the Object that is the interface between Unity and Android private UnityPlayer mUnityPlayer; // not needed in FragmentManager (Ultimate) implementation // private Activity thisActivity; //the two fragments private UnityFragment unityFragment; private AndroidButtonFragment androidButtonFragment;

The procedure goes:

  1. Enable main content layout and find container where to put Fragment(s) (depending on branch)
setContentView(R.layout.activity_main);
FrameLayout container = (FrameLayout) findViewById(R.id.container);
  1. Instantiate and initialize UnityPlayer
mUnityPlayer = new UnityPlayer(this);
int glesMode = mUnityPlayer.getSettings().getInt("gles_mode", 1);
mUnityPlayer.init(glesMode, false);
  1. Initialize fragments and add to UnityPlayerActivity's own FragmentManager when the UnityPlayerActivity is newly created
if (savedInstanceState == null || getFragmentManager().findFragmentByTag("unity") == null || (getFragmentManager().findFragmentByTag("android") == null)) {
          unityFragment = UnityFragment.newInstance(mUnityPlayer);
          androidButtonFragment = AndroidButtonFragment.newInstance(mUnityPlayer);
          //mUnityPlayer.pause();
          getFragmentManager().beginTransaction()
                  .add(container.getId(), unityFragment)
                  .add(container.getId(), androidButtonFragment)
                  .commit();
      }
  1. Offer methods that can be called by Fragments and Unity itself to switch between (by hiding the other) fragments.
public void switchFragmentToUnity(){
  //for good measure, not necessary
  mUnityPlayer.resume();
  getFragmentManager().beginTransaction().hide(androidButtonFragment).show(unityFragment).commit();
}
 public void callMeNonStatic(String s){
      Log.d("Non-Static", "Non-Static Call from Unity at " + s);
  /* Easy way
      thisActivity.runOnUiThread(new Runnable() {
          @Override
          public void run() {
              container.setVisibility(View.GONE);
          }
      });*/

  // Harder way (possibly more powerful)
      getFragmentManager().beginTransaction().hide(unityFragment).show(androidButtonFragment).commit();
      //mUnityPlayer.pause();
  }
  // and static;
  public static void callMeStatic(String s){
      Log.d("Static", "Static Call from Unity at " +  s);
  }
  1. Furthermore I have overridden the back-button to allow transitioning between the Fragments as demonstration
@Override
  public void onBackPressed(){
      Log.d("Input", "Back button pressed");
      //mUnityPlayer.pause();
      FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
      // Breaks stuff, use at your own peril!
      //fragmentTransaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
      if(getFragmentManager().findFragmentByTag("android").isHidden())
      {
          fragmentTransaction.hide(unityFragment).show(androidButtonFragment).commit();
      } else
      {
          fragmentTransaction.hide(androidButtonFragment).show(unityFragment).commit();
      }
      //container.setVisibility(View.GONE);
  }
  1. Lastly UnityPlayerActivity contains overriden lifeCycle Methods in order to properly manage the UnityPlayer during different points in the apps' lifecycle, for instance when switching app. Explain what these tests test and why

UnityFragment

UnityFragment is a very simple android.app.Fragment implementation, overriding the onCreateView method to find a View where to paste the UnityPlayer's view on in order to return a view containing the UnityPlayer's graphics.

@Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_layout, container, false);

        FrameLayout layout = (FrameLayout) view.findViewById( R.id.frameLayout );
        layout.addView(mUnityPlayer, 0, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));

        //mUnityPlayer.resume();

        return view;
    }

UnityPlayer is passed through an extra initialization method (don't use default constructor in fragments). This could be improved upon by creating a UnityPlayer singleton class which contains all methods pertaining the Unity part of the project.

 //initialize once when Fragment is built for the first time in activity
    public static UnityFragment newInstance(UnityPlayer unityPlayer){
        UnityFragment unityFragment = new UnityFragment();
        unityFragment.mUnityPlayer = unityPlayer;

        return unityFragment;
    }

AndroidButtonFragment

Simple fragment containing once again an initialization method (for the same reason as above)

public static AndroidButtonFragment newInstance(UnityPlayer unityPlayer) {
        AndroidButtonFragment androidButtonFragment = new AndroidButtonFragment();
        androidButtonFragment.mUnityPlayer = unityPlayer;
      //  androidBtnFragment = androidButtonFragment;
        return androidButtonFragment;
    }

and an overridden onCreateView which inflates and attaches the view to a View object which can be returned to the main UnityPlayerActivity. Furthermore finds the btnUnity (called thus in .xml) and attaches a listener to it which in turn triggers an (anonymous) inner class that calls UnityPlayerActivity in order to hide the Android fragment and show the Unity one.

        Button buttonUnity = (Button) view.findViewById(R.id.btnUnity);
        buttonUnity.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                UnityPlayerActivity activity = (UnityPlayerActivity) getActivity();
                activity.switchFragmentToUnity();
            }
        });

Author

Michael Klenk - mdklenk

License

License for demonstration purposes in a non-commercial setting only.

Acknowledgments

Thanks to numerous users (too many to list) both on github and stackoverflow whose postings made it somewhat understandable to go about implementing this.

About

Run in background test

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages