Friday, August 2, 2013

Building mobile game engine with Android NDK: 2 - calling C++ library from Java



all parts:
1 - installing the tools and running samples
2 - calling C++ library from Java
3 - abstracting types and logging
4 - timing service


 In this part we will start coding of our engine. When building Android NDK engine you can make fully native application or use more or less java. In my engine I am using very thin java layer and I will do the same in this tutorial. So despite the name of this article is promising C/C++ programming we will stick to java most of the time today yet.
 We will create simple java classes for handling application lifecycle and we will initialize OpenGL. 

Project

 Start with New -> Other ... -> Android -> Android Activity. Choose "Android Application Project" and fill the name of the project as well as java package name. Also set min and target SDKs:

 Then click "Next" as long as it is needed. You will end with project created in left panel. I named the project  "TutGame". So, if you want to follow me as closely as possible, name it with the same name.


Game x Engine

 From the beginning we will strictly divide our programming between engine and concrete game. Later you will see that engine is divided to platform specific part and general platform independent part. For now we have in "src" directory just one source file "TutGame.java". Go inside and replace it with this code:

package com.sbcgames.tutgame;

import android.os.Bundle;

import com.sbcgames.sbcengine.SBCEngine;

public class TutGame extends SBCEngine
{
 //------------------------------------------------------------------------
 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
  super.onCreate(savedInstanceState);
 }
}

 You will get several (four) errors. It is ok, as referenced classes do not exist yet. This class is now concrete game we want to run in our nonexistent engine. Every new game will have its own name and manifest but will extend common engine class (SBCEngine).

 Next we have to make some changes into AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:myapp="http://schemas.android.com/apk/res/com.sbcgames.tutgame.TutGame"
    package="com.sbcgames.tutgame"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0" >

    <!-- to install on SD card  -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
    <uses-sdk
        android:minSdkVersion="9"
        android:targetSdkVersion="18" />

    <!-- Tell the system this app requires OpenGL ES 2.0. -->
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
    
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
        
        <activity
            android:name="TutGame"
            android:configChanges="orientation|keyboard|keyboardHidden"
            android:label="@string/app_name"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" >

            <meta-data
                android:name="android.app.lib_name"
                android:value="sbcengine" />
            
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            
        </activity>
        
    </application>

</manifest>

 From the highlighted points we:
  • said that we prefer installation into external storage (and we added required permission few lines later),
  • we say that target device has to have OpenGL 2.0 support,
  • we want full screen for our application,
  • we say that changes in orientation and changes in keyboard configuration will be handled by us, otherwise the activity would restart on this. For example if we changed the orientation of device from portrait to landscape the activity would restart itself without this line,
  • the activity will run in portrait mode,
  • we say the name of our native library with C/C++ code for game and engine
 The manifest is game specific.  While the next step is engine specific.

 Go to "res -> layout" directory and delete "activity_tut_game.xml" file and create new "main.xml" file with the following content:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.sbcgames.sbcengine.SBCGLView
        android:id="@+id/sbcglview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

 Here we just defined our layout that will have SBCGLView as the view class stretched over the whole screen. We will create the class shortly.


C/C++ Engine part

 Now let's jump into C. Create new directory on the same level as "src" and name it "jni". This is where all our C code will reside. Create three empty files in it:
  • Main.cpp
  • Android.mk
  • Application.mk
 Your project now contains cpp file but the project is pure java project. You must go to New -> Other ... -> C/C++ -> Convert to C/C++ Project (Adds C/C++ Nature). You also must set all the paths to headers and set correctly the build command. All these steps were described in first part of the tutorial.

 After this you cen go into Application.mk and put these lines into it:

APP_PLATFORM := android-9
APP_STL      := gnustl_static

# Build for target machines.
#APP_ABI := all
APP_ABI := armeabi
#APP_ABI := x86

 While java apps are running on ARM devices as well as Intel based devices, this is not true for C/C++ apps. You have to build it for every HW platform it will run on. Fortunately you can place libraries for all the platforms into one installation APK and the correct one is loaded at runtime. Here you are selecting for which platforms you want to do the build. When developing you will probably build only for one platform as it takes more time to build for all every time. Default Android emulators eat ARM code. But there is also for example accelerated emulator of Intel based devices so you may build primary for it (x86).

 Now go into Android.mk and put these lines into it:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

# final library name = libengine.so
LOCAL_MODULE    := sbcengine

# turn warning into errors
LOCAL_CFLAGS    := -Werror
LOCAL_CFLAGS    := -Wno-psabi

# build all include paths
# add 4 levels of nesting
# seems to be better way than include all the files one by one
FILE_LIST := $(wildcard $(LOCAL_PATH)/*.cpp)
FILE_LIST += $(wildcard $(LOCAL_PATH)/**/*.cpp)
FILE_LIST += $(wildcard $(LOCAL_PATH)/**/**/*.cpp)
FILE_LIST += $(wildcard $(LOCAL_PATH)/**/**/**/*.cpp)

LOCAL_SRC_FILES := $(FILE_LIST:$(LOCAL_PATH)/%=%)

# include android libraries
# for logging
LOCAL_LDLIBS    += -llog
# for EGL
LOCAL_LDLIBS    += -lEGL
#for openGL (1 or 2)
#LOCAL_LDLIBS    += -lGLESv1_CM
LOCAL_LDLIBS    += -lGLESv2
# for native app
LOCAL_LDLIBS    += -landroid
#openSL
LOCAL_LDLIBS    += -lOpenSLES
#Zlib
LOCAL_LDLIBS    += -lz

include $(BUILD_SHARED_LIBRARY)

 What we do here is that we defined name of our native library (sbcengine) and then we say we want to include all source files (with 4 levels of nesting) into build. In the end we link needed libraries. We are now linking far more than is used in this part of tutorial (like zlib packing library and so on). But in later parts we will need it and we have it linked now.

 It is time to open Main.cpp and put all this into it:

#include <jni.h>
#include <errno.h>
#include <stdio.h>
#include <android/sensor.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>


//forward declarations
static void engine_tick(JNIEnv* aEnv, jobject aObj);
static void engine_resume(JNIEnv* aEnv, jobject aObj);
static void engine_pause(JNIEnv* aEnv, jobject aObj);
static void engine_start(JNIEnv* aEnv, jobject aObj, jobject aAssetManager);
static void engine_stop(JNIEnv* aEnv, jobject aObj, jboolean aTerminating);
static void engine_run(JNIEnv* aEnv, jobject aObj);
static void engine_set_screen_size(JNIEnv* aEnv, jobject aObj, jint aWidth, jint aHeight);

//------------------------------------------------------------------------
static void engine_resume(JNIEnv* aEnv, jobject aObj)
{

}

//------------------------------------------------------------------------
static void engine_pause(JNIEnv* aEnv, jobject aObj)
{

}

//------------------------------------------------------------------------
static void engine_start(JNIEnv* aEnv, jobject aObj, jobject aAssetManager)
{
 AAssetManager* assetManager = AAssetManager_fromJava(aEnv, aAssetManager);
}

//------------------------------------------------------------------------
static void engine_stop(JNIEnv* aEnv, jobject aObj, jboolean aTerminating)
{

}

//------------------------------------------------------------------------
static void engine_tick(JNIEnv* aEnv, jobject aObj)
{
 // Just fill the screen with a color.
 glClearColor(0.5f, 1.0f, 0.5f, 1.0f);
 glClear(GL_COLOR_BUFFER_BIT);
}

//------------------------------------------------------------------------
static void engine_run(JNIEnv* aEnv, jobject aObj)
{

}

//------------------------------------------------------------------------
static void engine_set_screen_size(JNIEnv* aEnv, jobject aObj, jint aWidth, jint aHeight)
{

}

//------------------------------------------------------------------------
extern "C"
{
JavaVM* gJavaVM = NULL;
jobject gJavaActivityClass;
const char* kJavActivityClassPath = "com/sbcgames/sbcengine/SBCEngine";

static JNINativeMethod methodTable[] = {
  {"engine_tick", "()V", (void *) engine_tick},
  {"engine_start", "(Landroid/content/res/AssetManager;)V", (void *) engine_start},
  {"engine_stop", "(Z)V", (void *) engine_stop},
  {"engine_pause", "()V", (void *) engine_pause},
  {"engine_resume", "()V", (void *) engine_resume},
  {"engine_run", "()V", (void *) engine_run},
  {"engine_set_screen_size", "(II)V", (void *) engine_set_screen_size},
};

//------------------------------------------------------------------------
jint JNI_OnLoad(JavaVM* aVm, void* aReserved)
{
 // cache java VM
 gJavaVM = aVm;

 JNIEnv* env;
 if (aVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
 {
  //LOGE("Failed to get the environment");
  return -1;
 }

 // Get SBCEngine activity class
 jclass activityClass = env->FindClass(kJavActivityClassPath);
 if (!activityClass)
 {
  //LOGE("failed to get %s class reference", kJavActivityClassPath);
  return -1;
 }

 // Register methods with env->RegisterNatives.
 env->RegisterNatives(activityClass, methodTable, sizeof(methodTable) / sizeof(methodTable[0]));
 gJavaActivityClass = env->NewGlobalRef(activityClass);

 return JNI_VERSION_1_6;
}

} // extern "C"

 In the top there is declaration of our main methods. Most of them is empty now except for engine_tick, where we are setting the color of background to have some reply after we run the app:

//------------------------------------------------------------------------
static void engine_tick(JNIEnv* aEnv, jobject aObj)
{
 // Just fill the screen with a color.
 glClearColor(0.5f, 1.0f, 0.5f, 1.0f);
 glClear(GL_COLOR_BUFFER_BIT);
}

 With line "extern "C"" starts part that that binds these methods for java to recognize them. I wrote separate detailed article which explains what is going on in JNI_OnLoad here. Just to say: JNI_OnLoad is called when the native library is loaded and you can do some setup here to make your life easier. We say here what our methods that will be called from java are and which parameters it takes.


Back to java

 We still have to do some work before we can run our first app. Go into "src" directory and create new package there. The package will name "com.sbcgames.sbcengine" and will have three files in it:
  • SBCEngine.java
  • SBCGLView.java
  • SBCRenderer.java
 First open SBCEngine.java and put this code into it:

package com.sbcgames.sbcengine;

import com.sbcgames.tutgame.R;

import android.app.Activity;
import android.content.res.AssetManager;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

public class SBCEngine extends Activity
{
 static
 {
  System.loadLibrary("sbcengine");
 }
 
 public static SBCEngine sbcEngine;
 
 private GLSurfaceView mSBCGLView = null;
 private AssetManager mAssetManager = null;

 native static void engine_tick();
 native static void engine_start(AssetManager aAssetManager);
 native static void engine_stop(boolean aTerminating);
 native static void engine_pause();
 native static void engine_resume();
 native static void engine_run();
 native static void engine_set_screen_size(int aWidth, int aHeight);

 //------------------------------------------------------------------------
 @Override
 protected void onCreate(Bundle savedInstance)
 {
  super.onCreate(savedInstance);

  sbcEngine = this;

  mAssetManager = getResources().getAssets();
  
  setContentView(R.layout.main);
                mSBCGLView = (GLSurfaceView) findViewById(R.id.sbcglview);
 }
 
 // ------------------------------------------------------------------------
 @Override
 protected void onPause()
 {
  super.onPause();
  
  mSBCGLView.onPause();
  
  if (isFinishing())
   engine_stop(true);
 }

 // ------------------------------------------------------------------------
 @Override
 protected void onResume()
 {
  super.onResume();
  
  mSBCGLView.onResume();
 }
 
 // ------------------------------------------------------------------------
 @Override
 public void onDestroy()
 {
  super.onDestroy();
 }
 
 // ------------------------------------------------------------------------
 public void startEngine()
 {
  engine_start(mAssetManager);
 }
 
 //------------------------------------------------------------------------
 static SBCEngine getSBCEngine()
 {
  return sbcEngine;
 }
 
 //-----------------------------------------------------
 public static int finishApp()
 {
  final SBCEngine engine = getSBCEngine();
  
  engine.runOnUiThread(new Runnable()
  {
   @Override
   public void run()
   {
    getSBCEngine().finish();
   }
  });
  
  return -1;
 }
}

 Now you see that this is engine class which will remain unchanged when writing game (unless you are adding new features into engine). This is the reason why for TutGame we created simple class that is extending this one.
 In the very top you can see line: "System.loadLibrary("sbcengine");" This loads our sbcengine library. Remember we defined the name in Android.mk file. When the library is loaded the JNI_OnLoad method is checked for presence. If present (as is in our case) it is called.
 In onCreate we are setting our previously created main.xml layout as the content of this activity.
 The rest are more or less simple methods handling the activity lifecycle or making our life easier later.

 Next open SBCRenderer.java and put this into it:

package com.sbcgames.sbcengine;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView;
import android.util.Log;

public class SBCRenderer implements GLSurfaceView.Renderer
{
 private boolean mInitialized = false;
 
 //-----------------------------------------------------
 public void onDrawFrame(GL10 aGL)
 {
  SBCEngine.engine_tick();
 }

 //-----------------------------------------------------
 public void onSurfaceChanged(GL10 aGL, int aWidth, int aHeight)
 {
  Log.d("SBCGLRenderer", "Changed");

  SBCEngine.engine_set_screen_size(aWidth, aHeight);
 }

 //-----------------------------------------------------
 public void onSurfaceCreated(GL10 aGL, EGLConfig aEglConfig)
 {
  Log.d("SBCGLRenderer", "Created");
  
  if (!mInitialized)
  {
   SBCEngine.getSBCEngine().startEngine();
   SBCEngine.engine_run();
   mInitialized = true;
  }
  SBCEngine.engine_resume();
 }
}

 Renderer is class with call back methods that will get called when OpenGL surface is created or changed and also on every frame - 60 times per second in ideal case. You can see that we are simply forwarding the call into the C/C++ library (SBCEngine.engine_tick();) where we are setting the screen background to visually see that our engine works.

 The final class SBCGLView.java is gluing SBCRenderer and SBCEngine together and it is also the place where OpenGL is initialized. Open the file and put this code into it (the code was done with copypasting from several examples):

package com.sbcgames.sbcengine;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import android.graphics.PixelFormat;

import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.util.Log;

public class SBCGLView extends GLSurfaceView
{
 private static final boolean OPENGL_VERSION_2 = true;
 
 //-----------------------------------------------------
 public SBCGLView(Context context)
 {
  super(context);

  init(false, 0, 0);
 }
 
 //-----------------------------------------------------
 public SBCGLView(Context context, AttributeSet attrs)
 {
  super(context, attrs);

  init(false, 0, 0);
 }
 
 //-----------------------------------------------------
 private void init(boolean aTranslucent, int aDepth, int aStencil)
 {
  if (OPENGL_VERSION_2)
  {
   /*
    * By default, GLSurfaceView() creates a RGB_565 opaque surface. If
    * we want a translucent one, we should change the surface's format
    * here, using PixelFormat.TRANSLUCENT for GL Surfaces is
    * interpreted as any 32-bit surface with alpha by SurfaceFlinger.
    */
   if (aTranslucent)
    this.getHolder().setFormat(PixelFormat.TRANSLUCENT);

   /*
    * Setup the context factory for 2.0 rendering. See ContextFactory
    * class definition below
    */
   setEGLContextFactory(new ContextFactory());

   /*
    * We need to choose an EGLConfig that matches the format of our
    * surface exactly. This is going to be done in our custom config
    * chooser. See ConfigChooser class definition below.
    */
   setEGLConfigChooser(aTranslucent ?
      new ConfigChooser(8, 8, 8, 8, aDepth, aStencil) :
      new ConfigChooser(5, 6, 5, 0, aDepth, aStencil)
   );
  }
  
        setRenderer(new SBCRenderer());
 }
 
 //-----------------------------------------------------
 @Override
 public void onPause()
 {
  super.onPause();
  // native pause
  SBCEngine.engine_pause();
  Log.d("SBCGLView", "View paused");
 }

 //-----------------------------------------------------
 @Override
 public void onResume()
 {
  super.onResume();
  // native resume
  Log.d("SBCGLView", "View resumed");
 }

 // -----------------------------------------------------
 private static void checkEglError(String aPrompt, EGL10 aEgl)
 {
  int error;
  
  while ((error = aEgl.eglGetError()) != EGL10.EGL_SUCCESS)
  {
   Log.e("SBCGLView", String.format("%s: EGL error: 0x%x", aPrompt, error));
  }
 }
 
 //-----------------------------------------------------
 // Context Factory
 //-----------------------------------------------------
 private static class ContextFactory implements GLSurfaceView.EGLContextFactory
 {
  private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;

  //-----------------------------------------------------
  public EGLContext createContext(EGL10 aEgl, EGLDisplay aDisplay, EGLConfig aEglConfig)
  {
   Log.w("SBCGLView", "creating OpenGL ES 2.0 context");
   
   checkEglError("Before eglCreateContext", aEgl);
   
   int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE};
   EGLContext context = aEgl.eglCreateContext(aDisplay, aEglConfig,
     EGL10.EGL_NO_CONTEXT, attrib_list);
   
   checkEglError("After eglCreateContext", aEgl);
   
   return context;
  }

  //-----------------------------------------------------
  public void destroyContext(EGL10 aEgl, EGLDisplay aDisplay, EGLContext aContext)
  {
   aEgl.eglDestroyContext(aDisplay, aContext);
  }
 }

 
 //-----------------------------------------------------
 // Config Chooser
 //-----------------------------------------------------
 private static class ConfigChooser implements GLSurfaceView.EGLConfigChooser
 {
  // Subclasses can adjust these values:
  protected int mRedSize;
  protected int mGreenSize;
  protected int mBlueSize;
  protected int mAlphaSize;
  protected int mDepthSize;
  protected int mStencilSize;
  private int[] mValue = new int[1];
  
  /*
   * This EGL config specification is used to specify 2.0 rendering. We
   * use a minimum size of 4 bits for red/green/blue, but will perform
   * actual matching in chooseConfig() below.
   */
  private static int EGL_OPENGL_ES2_BIT = 4;
  private static int[] s_configAttribs2 = {
   EGL10.EGL_RED_SIZE, 4,
   EGL10.EGL_GREEN_SIZE, 4,
   EGL10.EGL_BLUE_SIZE, 4,
   EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
   EGL10.EGL_NONE };
  
  //-----------------------------------------------------
  public ConfigChooser(int aRed, int aGreen, int aBlue, int aAlpha, int aDepth, int aStencil)
  {
   mRedSize = aRed;
   mGreenSize = aGreen;
   mBlueSize = aBlue;
   mAlphaSize = aAlpha;
   mDepthSize = aDepth;
   mStencilSize = aStencil;
  }

  //-----------------------------------------------------
  public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display)
  {
   // Get the number of minimally matching EGL configurations
   int[] num_config = new int[1];
   egl.eglChooseConfig(display, s_configAttribs2, null, 0, num_config);

   int numConfigs = num_config[0];

   if (numConfigs <= 0)
    throw new IllegalArgumentException("No configs match configSpec");

   // Allocate then read the array of minimally matching EGL configs
   EGLConfig[] configs = new EGLConfig[numConfigs];
   egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config);

   // return the "best" one
   return chooseConfig(egl, display, configs);
  }

  //-----------------------------------------------------
  public EGLConfig chooseConfig(EGL10 aEgl, EGLDisplay aDisplay, EGLConfig[] aConfigs)
  {
   EGLConfig backupConfig = null;
   
   for (EGLConfig config : aConfigs)
   {
    int d = findConfigAttrib(aEgl, aDisplay, config, EGL10.EGL_DEPTH_SIZE, 0);
    int s = findConfigAttrib(aEgl, aDisplay, config, EGL10.EGL_STENCIL_SIZE, 0);

    // We need at least mDepthSize and mStencilSize bits
    if (d < mDepthSize || s < mStencilSize)
     continue;

    // We want an *exact* match for red/green/blue/alpha
    int r = findConfigAttrib(aEgl, aDisplay, config, EGL10.EGL_RED_SIZE, 0);
    int g = findConfigAttrib(aEgl, aDisplay, config, EGL10.EGL_GREEN_SIZE, 0);
    int b = findConfigAttrib(aEgl, aDisplay, config, EGL10.EGL_BLUE_SIZE, 0);
    int a = findConfigAttrib(aEgl, aDisplay, config, EGL10.EGL_ALPHA_SIZE, 0);

    // backup config that should exist every time
    if (r == 5 && g == 6 && b == 5 && a == 0)
     backupConfig = config;
    
    if (r == mRedSize && g == mGreenSize && b == mBlueSize && a == mAlphaSize)
     return config;
   }
   
   Log.w("SBCGLView", "returning backup config");
   return backupConfig;
  }

  //-----------------------------------------------------
  private int findConfigAttrib(EGL10 aEgl, EGLDisplay aDisplay, EGLConfig aConfig,
    int aAttribute, int aDefaultValue)
  {
   if (aEgl.eglGetConfigAttrib(aDisplay, aConfig, aAttribute, mValue))
    return mValue[0];

   return aDefaultValue;
  }
 }
}

 The code is long but all it does is that it sets EGL context, chooses right configuration and sets renderer. Configuration says how much bits is required for red, green or blue color or for depth.


Running

 This was the last piece we needed. You can now build the example and run it in emulator. If everything went OK you should have screen like this:
  While the screen looks statically, it is updated every frame - but the only thing we do is we clear background with green color.


Conclusion

 Today we set basic framework for our engine. There will also be some updates in java part later but there will be less and less of them. Most of the work will move to C/C++. You can download the whole project here:

Download TutGame-02.zip



2 comments:

  1. Wonderful work! I am enjoying reading your tutorials, keep it up!

    ReplyDelete
  2. In the realm of academic endeavors, the services of dissertation writers for hire prove indispensable. These professionals bring a wealth of knowledge to the table, ensuring your research is not only comprehensive but also presented with the finesse required for scholarly excellence. With their assistance, navigating the intricate path of dissertation writing becomes a smoother and more rewarding journey.

    ReplyDelete