Android NDK Game Development Cookbook

5 (1 reviews total)
By Sergey Kosarevsky , Viktor Latypov
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Establishing a Build Environment

About this book

Android NDK is used for multimedia applications which require direct access to a system's resources. Android NDK is also the key for portability, which in turn provides a reasonably comfortable development and debugging process using familiar tools such as GCC and Clang toolchains. If your wish to build Android games using this amazing framework, then this book is a must-have.

This book provides you with a number of clear step-by-step recipes which will help you to start developing mobile games with Android NDK and boost your productivity debugging them on your computer. This book will also provide you with new ways of working as well as some useful tips and tricks that will demonstrably increase your development speed and efficiency.

This book will take you through a number of easy-to-follow recipes that will help you to take advantage of the Android NDK as well as some popular C++ libraries. It presents Android application development in C++ and shows you how to create a complete gaming application.

You will learn how to write portable multithreaded C++ code, use HTTP networking, play audio files, use OpenGL ES, to render high-quality text, and how to recognize user gestures on multi-touch devices. If you want to leverage your C++ skills in mobile development and add performance to your Android applications, then this is the book for you.

Publication date:
November 2013
Publisher
Packt
Pages
320
ISBN
9781782167785

 

Chapter 1. Establishing a Build Environment

 

Some LinkedIn profiles say developing with a particular IDE is a skill.

No! Development without any IDE is the skill!

 
 --Sergey Kosarevsky

In this chapter, we will cover the following recipes:

  • Installing Android development tools on Windows

  • Installing Android development tools on Linux

  • Creating an application template manually

  • Adding native C++ code to your application

  • Switching NDK toolchains

  • Supporting multiple CPU architectures

  • Basic rendering with OpenGL ES

  • Going cross platform

  • Unifying the cross-platform code

  • Linking and source code organization

  • Signing release Android applications

 

Introduction


This chapter explains how to install and configure Android NDK on Microsoft Windows or Ubuntu/Debian Linux, and how to build and run your first application on an Android-based device. We will learn how to set-up different compilers and toolchains that come with Android NDK. In addition, we show how to setup the GCC toolchain for Windows to build your projects. The rest of the chapter is devoted to cross-platform development using C++.

 

Installing Android development tools on Windows


To start developing games for Android you will need some essential tools to be installed on your system.

Getting ready

Here is the list of all the prerequisites you will need to start developing games for Android:

Former versions of SDK/NDK for Windows required a Cygwin environment, a Linux-like environment for Windows, to be installed. Up-to-date versions of these tools can run natively on Windows without any intermediate layer. We will focus on the Cygwin-less environment and will do all of the development without IDE. You heard it right, we will just use the command line. All the examples in this book were written and debugged on a Windows PC.

To compile native Windows applications presented in this book, you will need a decent C++ compiler, such as the MinGW package with a GCC toolchain. Using Microsoft Visual Studio is also possible.

Note

Minimalist GNU for Windows (MinGW) is a minimalist development environment for Windows applications using a port of GNU Compiler Collection (GCC).

How to do it...

  1. Android SDK and NDK should be installed into folders that do not contain any whitespaces in their names.

    Note

    This requirement comes from the limitations of scripts in Android SDK. There is a nice discussion on StackOverflow which explains some reasons behind these limitations at http://stackoverflow.com/q/6603194/1065190.

  2. Other tools can be installed to their default locations. We used the following paths in our Windows 7 system:

Tools

Path

Android SDK

D:\android-sdk-windows

Android NDK

D:\ndk

Apache Ant

D:\ant

Java Development Kit

C:\Program Files\Java\jdk1.6.0_33

All tools have pretty decent GUI installers (see the following image, that shows the Android SDK Manager from SDK R21) so you don't have to use the command line.

For the Windows environment, you need the MinGW GCC toolchain. The easy to install all-in-one package can be found at http://www.equation.com, in the Programming Tools section, Fortran, C, C++ subsection. Alternatively, you can download the official installer from http://www.mingw.org. We will use the one from www.equation.com

There's more...

You need to set some environment variables to let the tools know where the files are located. The JAVA_HOME variable should point to the Java Development Kit folder. The NDK_HOME variable should point to the Android NDK installation folder, and ANDROID_HOME should point to the Android SDK folder (note the double backslash). We used the following environment variable values:

JAVA_HOME=D:\Java\jdk1.6.0_23

NDK_HOME=D:\ndk

ANDROID_HOME=D:\\android-sdk-windows

The final configuration looks similar to the one shown in the following screenshot, which shows the Windows Environment Variables dialog box:

After MinGW has been successfully installed, you should also add the bin folder from its installation folder to the PATH environment variable. For example, if MinGW is installed to C:\MinGW, then PATH should contain the C:\MinGW\bin folder.

 

Installing Android development tools on Linux


Installation of the basic tools on Linux is as easy as it was with their Windows counterpart. In this recipe, we will see how to install the basic Android development tools on *nix systems.

Getting ready

We assume you already have an Ubuntu/Debian system with the apt package manager. Refer to http://wiki.debian.org/Apt for details.

How to do it...

Carry out the following steps to install the required basic tools:

  1. Make sure you are using the latest version of the packages for your OS by running the following command:

    >sudo apt-get update
    
  2. Install OpenJDK 6+:

    >sudo apt-get install openjdk-6-jdk
    
  3. Install the Apache Ant build automation tool:

    >sudo apt-get install ant
    
  4. Download the official Android SDK from http://developer.android.com. There is a bigger package next to it, with the ADT plugin for the Eclipse IDE. However, since we do all of our development from the command line, we won't need it. Run the following command:

    >wget http://dl.google.com/android/android-sdk_r22.2.1-linux.tgz
    
  5. Unpack the downloaded .tgz file (the actual version might vary, 22.2.1 is the latest version as of October 2013):

    >tar -xvf android-sdk_r22.2.1-linux.tgz
    
  6. Use ~/<sdk>/tools/android to install the latest Platform Tools and all of the SDKs—just like in the Windows case.

    Failure to do so will result in an error while trying to use the Ant tool when building any application for the Android.

  7. Get the official Android NDK from http://developer.android.com:

    >wget http://dl.google.com/android/ndk/android-ndk-r9b-linux-x86_64.tar.bz2
    
  8. Unpack the downloaded NDK .tgz file:

    >tar -xvf android-ndk-r9b-linux-x86_64.tar.bz2
    
  9. Set the NDK_ROOT environment variable to your Android NDK directory (for example, ~/android-ndk-r9b in our case):

    >NDK_ROOT=/path/to/ndk
    

    It is useful to put this line and the JAVA_HOME definition to /etc/profile or /etc/environment, if these settings are applicable to all the users of the system.

  10. In case you are running a 64-bit system, you must ensure that you have the 32-bit Java runtime installed also.

  11. Run the following command to install the libraries. Failure to do so may lead to errors with adb and aapt tools:

    >sudo apt-get install ia32-libs
    

There's more...

There is a nice one-liner script that helps you automatically detect the OpenJDK home directory. It essentially resolves the link /usr/bin/javac to the full path and returns the directory part of the path.

  JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:bin/javac::")
 

Creating an application template manually


First of all, we are going to create a basic template for our applications. Every Android application that is to be built via Android SDK, should contain a predefined directory structure and the configuration .xml files. This can be done using Android SDK tools and IDEs. In this recipe, we will learn how to do it manually. We will use these files later on as the very starting point for all our examples.

Getting ready

Let us set up the directory structure of our project (see the following screenshot):

This is a typical structure for any Android project. We will create all the required files manually rather than using Android tools.

How to do it...

Place the Java Activity code into the App1\src\com\packtpub\ndkcookbook\app1\App1Activity.java file, which should look as follows:

package com.packtpub.ndkcookbook.app1;
import android.app.Activity;
public class App1Activity extends Activity
{
};

The localizable application name should go to App1\res\values\strings.xml. The string parameter app_name is used in the AndroidManifest.xml file to specify the user-readable name of our application, as seen in the following code:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">App1</string>
</resources>

Now we need to write more scripts for Apache Ant and the Android SDK build system. They are necessary to build the .apk package of your application.

  1. The following is the App1/project.properties file:

    target=android-15
    sdk.dir=d:/android-sdk-windows
  2. We need two more files for Ant. The following is App1/AndroidManifest.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.packtpub.ndkcookbook.app1"
      android:versionCode="1"
      android:versionName="1.0.0">
      <supports-screens
         android:smallScreens="false"
         android:normalScreens="true"
         android:largeScreens="true"
         android:xlargeScreens="true"
         android:anyDensity="true" />
      <uses-sdk android:minSdkVersion="8" />
      <uses-sdk android:targetSdkVersion="18" />

    Our examples require at least OpenGL ES 2. Let Android know about it:

      <uses-feature android:glEsVersion="0x00020000"/>
      <application android:label="@string/app_name"
                   android:icon="@drawable/icon"
                   android:installLocation="preferExternal"
                   android:largeHeap="true"
                   android:debuggable="false">
      <activity android:name="com.packtpub.ndkcookbook.app1.App1Activity"
    android:launchMode="singleTask"

    Create a full-screen application in a landscape screen orientation:

                      android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                      android:screenOrientation="landscape"
                      android:configChanges="orientation|keyboardHidden"
                      android:label="@string/app_name">
        <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
         </intent-filter>
       </activity>
     </application>
    </manifest>

    The second file is App1/build.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <project name="App1" default="help">
        <property file="ant.properties" />
        <loadproperties srcFile="project.properties" />
        <import file="${sdk.dir}/tools/ant/build.xml" />
    </project>

How it works...

With all the listed files in place, we can now build the project and install it on an Android device by carrying out the following steps:

  1. From the App1 folder run:

    >ant debug
    
  2. The tail of the output from the previous command should look like:

    BUILD SUCCESSFUL
    Total time: 12 seconds
    
  3. And the built debug .apk package is in bin/App1-debug.apk.

  4. To install the app, run:

    >adb install App1-debug.apk
    

    Note

    Don't forget to connect your device through a USB and turn USB Debugging on in Android settings before running this command.

  5. You should see the output from adb, similar to the following commands:

    * daemon not running. starting it now on port 5037 *
    * daemon started successfully *
    1256 KB/s (8795 bytes in 0.006s)
            pkg: /data/local/tmp/App1-debug.apk
    Success
    

The application can now be started from your Android launcher (named App1). You will see just a black screen. You can exit the application using the BACK button.

There's more...

Don't forget to put the application icon into App1\res\drawable\icon.png. Refer to the book's code bundle if you want to build the app quickly, or put your own icon there. 72 x 72 32-bit will do just fine. You can find the official Android icons guidelines at http://developer.android.com/design/style/iconography.html.

The official documentation on the AndroidManifest.xml file can be found at http://developer.android.com/guide/topics/manifest/manifest-intro.html.

Furthermore, you can update your applications without uninstalling the previous version using the adb -r command-line switch in the following way:

>adb install -r App1-debug.apk

Otherwise, before installing a new version of your application you will have to uninstall the existing one using the following command:

>adb uninstall <package-name>

See also…

  • Signing release Android applications

 

Adding native C++ code to your application


Let us expand our minimalistic Java template, which was discussed in the previous recipe, so we can create a placeholder for our native C++ code.

Getting ready

We need to copy all the files from our App1 project to save time while creating the initial project files. This recipe will focus on the changes to be made to the App1 project in order to add the C++ code to it.

How to do it...

Carry out the following steps to create a placeholder for our C++ code:

  1. Add the jni/Wrappers.cpp file with the following code:

    #include <stdlib.h>
    #include <jni.h>
    #include <android/log.h>
    #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "App2", __VA_ARGS__))
    
    extern "C"
    {
      JNIEXPORT void JNICALL
    Java_com_packtpub_ndkcookbook_app2_App2Activity_onCreateNative( JNIEnv* env, jobject obj )
        {
          LOGI( "Hello World!" );
        }
    }
  2. We need to change our Activity class from the previous recipe to make use of the native code we just added in the preceding section, through the following code:

    package com.packtpub.ndkcookbook.app2;
    
    import android.app.Activity;
    import android.os.Bundle;
    
    public class App2Activity extends Activity
    {
        static
        {

    Here we load the native library named libApp2.so. Note the omitted lib prefix and .so extension:

          System.loadLibrary( "App2" );
        }
        @Override protected void onCreate( Bundle icicle )
        {
          super.onCreate( icicle );
          onCreateNative();
        }
        public static native void onCreateNative();
    };
  3. Tell the NDK build system how to treat the .cpp file. Create the jni/Android.mk file. The Android.mk file is used by the Android NDK build system to find out how to treat the source code of your project:

    TARGET_PLATFORM := android-7
    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_ARM_MODE := arm
    LOCAL_MODULE     := App2
    LOCAL_SRC_FILES += Wrappers.cpp
    LOCAL_ARM_MODE := arm
    COMMON_CFLAGS := -Werror -DANDROID -DDISABLE_IMPORTGL \
    -isystem $(SYSROOT)/usr/include/
    ifeq ($(TARGET_ARCH),x86)
    	LOCAL_CFLAGS   := $(COMMON_CFLAGS)
      else
    	LOCAL_CFLAGS   := -mfpu=vfp -mfloat-abi=softfp \
      -fno-short-enums $(COMMON_CFLAGS)
    endif
    LOCAL_LDLIBS     := -llog -lGLESv2 -Wl,-s
    LOCAL_CPPFLAGS += -std=gnu++0x
    include $(BUILD_SHARED_LIBRARY)

    Note the ifeq ($(TARGET_ARCH),x86) section. Here we specify architecture-specific compiler flags for floating point support on ARMv7. This will give you hardware floating-point support on the ARM architecture and a warnings-free log on the x86 Android target architecture..

  4. Paste the following code into the jni/Application.mk file:

    APP_OPTIM := release
    APP_PLATFORM := android-7
    APP_STL := gnustl_static
    APP_CPPFLAGS += -frtti 
    APP_CPPFLAGS += -fexceptions
    APP_CPPFLAGS += -DANDROID
    APP_ABI := armeabi-v7a
    APP_MODULES := App2
    NDK_TOOLCHAIN_VERSION := clang

How it works...

  1. First of all, we need to compile the native code. From the root of your App2 project, run the following command:

    >ndk-build
    
  2. You should see the following output:

    Compile++ arm: App2 <= Wrappers.cpp
    SharedLibrary: libApp2.so
    Install      : libApp2.so => libs/armeabi-v7a/libApp2.so
    
  3. Now proceed to the .apk creation as in the previous recipe by running the following command:

    >ant debug
    
  4. Your libApp2.so native shared library will be packed into the App2-debug.apk package. Install and run it. It will output a Hello World! string into the device log.

There's more...

You can use the adb command to view the device log. A nice clean formatted log with timestamps can be created using the following command:

>adb logcat -v time > 1.txt

The actual output from your device will look similar to the following command:

05-22 13:00:13.861 I/App2    ( 2310): Hello World!
 

Switching NDK toolchains


A toolchain is a set of tools that are used to build your project. A toolchain usually consists of a compiler, an assembler, and a linker. Android NDK comes with different toolchains—GCC and Clang—of different versions. It has a convenient and simple way to switch between them.

Getting ready

Look through the list of the available toolchains before proceeding. You can find all the available toolchains in the $(NDK_ROOT)/toolchains/ folder.

How to do it...

The parameter NDK_TOOLCHAIN_VERSION in Application.mk corresponds to one of the available toolchains. In NDK r9b, you can switch between three GCC versions—4.6, and 4.7, which are marked as deprecated and will be removed from the next NDK releases, and 4.8. And two Clang versions—Clang3.2, which is also marked as deprecated, and Clang3.3. The default toolchain in the NDK r9b is still GCC 4.6.

Starting from the NDK r8e, you can just specify clang as the value of NDK_TOOLCHAIN_VERSION. This option will select the most recent version of the available Clang toolchain.

There's more...

The toolchains are discovered by the $(NDK_ROOT)/build/core/init.mk script, so you can define your own toolchain in a folder named <ABI>-<ToolchainName> and use it in Application.mk.

 

Supporting multiple CPU architectures


Android NDK supports different CPU architectures such as ARMv5TE and ARMv7-based devices, x86, and MIPS (big-endian architecture). We can create fat binaries that can run on any of the supported platforms.

Getting ready

Find out the architecture of your Android-based device. You can do it using the adb command as follows:

>adb shell cat /proc/cpuinfo

How to do it...

The following are the two approaches to pick an appropriate set of CPU architectures:

  1. By default, the NDK will generate the code for ARMv5TE-based CPUs. Use the parameter APP_ABI in Application.mk to select a different architecture, for example (use only one line from the following list):

    APP_ABI := armeabi-v7a
    APP_ABI := x86
    APP_ABI := mips
  2. We can specify multiple architectures to create a fat binary that will run on any of them through the following command:

    APP_ABI := armeabi armeabi-v7a x86 mips

There's more...

The main pitfall of the fat binaries is the resulting .apk size, as separate native code versions are compiled for each of the specified architectures. If your application heavily uses third-party libraries, the package size can become an issue. Plan your deliverables wisely.

 

Basic rendering with OpenGL ES


Let us add some graphics to our sample Android application App2. Here, we show how to create an off-screen bitmap, and then copy it to the screen using the OpenGL ES Version 2 or 3 available on your Android device.

Note

Refer to the App3 sample in the book's downloadable code bundle for the full source code.

Getting ready

We assume that the reader is somewhat familiar with OpenGL and the GL Shading Language (GLSL). Refer to http://www.opengl.org/documentation for the desktop OpenGL, and http://www.khronos.org/opengles for the mobile OpenGL ES documentation.

How to do it…

  1. We need to write a simple vertex and fragment GLSL shader that will render our framebuffer on the screen using OpenGL ES. Let's put them directly into jni/Wrappers.cpp as strings. The following code shows the vertex shader:

    static const char g_vShaderStr[] =
       "#version 100\n"
       "precision highp float;\n"
       "attribute vec3 vPosition;\n"
       "attribute vec3 vCoords;\n"
       "varying vec2 Coords;\n"
       "void main()\n"
       "{\n"
       "   Coords = vCoords.xy;\n"
       "   gl_Position = vec4( vPosition, 1.0 );\n"
       "}\n";
  2. The fragment shader is as follows:

    static const char g_fShaderStr[] =
       "#version 100\n"
       "precision highp float;\n"
       "varying vec2 Coords;\n"
       "uniform sampler2D Texture0;\n"
       "void main()\n"
       "{\n"
       "   gl_FragColor = texture2D( Texture0, Coords );\n"
       "}\n";
  3. We will also need the following helper function to load our shaders into OpenGL ES:

    static GLuint LoadShader( GLenum type, const char* shaderSrc )
    {
       GLuint shader = glCreateShader( type );
       glShaderSource ( shader, 1, &shaderSrc, NULL );
       glCompileShader ( shader );
       GLint compiled;
       glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );
       GLsizei MaxLength = 0;
       glGetShaderiv( shader, GL_INFO_LOG_LENGTH, &MaxLength );
       char* InfoLog = new char[MaxLength];
       glGetShaderInfoLog( shader, MaxLength, &MaxLength, InfoLog );
       LOGI( "Shader info log: %s\n", InfoLog );
       return shader;
    }

How it works…

We will not go into all the details about the OpenGL ES programming here, and will instead focus on a minimal application (App3) that should initialize the GLView in Java; create fragment and vertex programs, create and fill the vertex array consisting of two triangles that form a single quadrilateral, and then render them with a texture, which is updated from g_FrameBuffer contents. This is it—just draw the offscreen framebuffer. The following is the code to draw the full-screen quad textured with the offscreen buffer content:

  const GLfloat vVertices[] = { -1.0f, -1.0f, 0.0f,
                                -1.0f,  1.0f, 0.0f,
                                 1.0f, -1.0f, 0.0f,
                                -1.0f,  1.0f, 0.0f,
                                1.0f, -1.0f, 0.0f,
                                1.0f,  1.0f, 0.0f
                              };

  const GLfloat vCoords[]   = {  0.0f,  0.0f, 0.0f,
                                 0.0f,  1.0f, 0.0f,
                                 1.0f,  0.0f, 0.0f,
                                 0.0f,  1.0f, 0.0f,
                                 1.0f,  0.0f, 0.0f,
                                 1.0f,  1.0f, 0.0f
                              };
  glUseProgram ( g_ProgramObject );

These attribute variables are declared in a vertex shader. See the value of g_vShaderStr[] in the preceding code.

  GLint Loc1 = glGetAttribLocation(g_ProgramObject,"vPosition");
  GLint Loc2 = glGetAttribLocation(g_ProgramObject,"vCoords");

  glBindBuffer( GL_ARRAY_BUFFER, 0 );
  glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
  glVertexAttribPointer(
    Loc1, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
  glVertexAttribPointer(
    Loc2, 3, GL_FLOAT, GL_FALSE, 0, vCoords   );
  glEnableVertexAttribArray( Loc1 );
  glEnableVertexAttribArray( Loc2 );

  glDisable( GL_DEPTH_TEST );
  glDrawArrays( GL_TRIANGLES, 0, 6 );
  glUseProgram( 0 );
  glDisableVertexAttribArray( Loc1 );
  glDisableVertexAttribArray( Loc2 );

We also need a few JNI callbacks. The first one handles the surface size changes, as seen in the following code:

  JNIEXPORT void JNICALLJava_com_packtpub_ndkcookbook_app3_App3Activity_SetSurfaceSize(JNIEnv* env, jclass clazz, int Width, int Height )
  {
    LOGI( "SurfaceSize: %i x %i", Width, Height );
    g_Width  = Width;
    g_Height = Height;
    GLDebug_LoadStaticProgramObject();
    glGenTextures( 1, &g_Texture );
    glBindTexture( GL_TEXTURE_2D, g_Texture );

Disable mip-mapping through the following code:

    glTexParameteri( GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA,ImageWidth, ImageHeight, 0, GL_RGBA,GL_UNSIGNED_BYTE, g_FrameBuffer );
  }

The second callback does the actual frame rendering:

  JNIEXPORT void JNICALL Java_com_packtpub_ndkcookbook_app3_App3Activity_DrawFrame( JNIEnv* env, jobject obj )
  {

Invoke our frame rendering callback through the following code:

    OnDrawFrame();

    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, g_Texture );
    glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0,ImageWidth, ImageHeight, GL_RGBA,GL_UNSIGNED_BYTE, g_FrameBuffer );
    GLDebug_RenderTriangle();
  }
 

Going cross platform


The main idea is the possibility of cross-platform development in What You See (on a PC) is What You Get (on a device), when most of the application logic can be developed in a familiar desktop environment like Windows, and it can be built for Android using the NDK whenever necessary.

Getting ready

To perform what we just discussed, we have to implement some sort of abstraction on top of the NDK, POSIX, and Windows API. Such an abstraction should feature at least the following:

  • Ability to render buffer contents on the screen: Our framework should provide the functions to build the contents of an off-screen framebuffer (a 2D array of pixels) to the screen (for Windows we refer to the window as "the screen").

  • Event handling: The framework must be able to process the multi-touch input and virtual/physical key presses (some Android devices, such as the Toshiba AC 100, or the Ouya console, and other gaming devices, have physical buttons), timing events, and asynchronous operation completions.

  • Filesystem, networking, and audio playback: The abstraction layers for these entities need a ton of work to be done by you, so the implementations are presented in Chapter 3, Networking, Chapter 4, Organizing a Virtual Filesystem, and Chapter 5, Cross-platform Audio Streaming.

How to do it...

  1. Let us proceed to write a minimal application for the Windows environment, since we already have the application for Android (for example, App1). A minimalistic Windows GUI application is the one that creates a single window and starts the event loop (see the following example in Win_Min1/main.c):

    #include <windows.h>
    
    LRESULT CALLBACK MyFunc(HWND h, UINT msg, WPARAM w, LPARAM p)
    {
      if(msg == WM_DESTROY) { PostQuitMessage(0); }
      return DefWindowProc(h, msg, w, p);
    }
    
    char WinName[] = "MyWin";
  2. The entry point is different from Android. However, its purpose remains the same— to initialize surface rendering and invoke callbacks:

    int main()
    {
      OnStart();
    
      const char WinName[] = "MyWin";
    
      WNDCLASS wcl;
      memset( &wcl, 0, sizeof( WNDCLASS ) );
      wcl.lpszClassName = WinName;
      wcl.lpfnWndProc = MyFunc;
      wcl.hCursor = LoadCursor( NULL, IDC_ARROW );
    
      if ( !RegisterClass( &wcl ) ) { return 0; }
    
      RECT Rect;
    
      Rect.left = 0;
      Rect.top = 0;
  3. The size of the window client area is predefined as ImageWidth and ImageHeight constants. However, the WinAPI function CreateWindowA() accepts not the size of the client area, but the size of the window, which includes caption, borders, and other decorations. We need to adjust the window rectangle to set the client area to the desired size through the following code:

      Rect.right  = ImageWidth;
      Rect.bottom = ImageHeight;
    
      DWORD dwStyle = WS_OVERLAPPEDWINDOW;
    
      AdjustWindowRect( &Rect, dwStyle, false );
    
      int WinWidth  = Rect.right  - Rect.left;
      int WinHeight = Rect.bottom - Rect.top;
    
      HWND hWnd = CreateWindowA( WinName, "App3", dwStyle,100, 100, WinWidth, WinHeight,0, NULL, NULL, NULL );
      ShowWindow( hWnd, SW_SHOW );
    
      HDC dc = GetDC( hWnd );
  4. Create the offscreen device context and the bitmap, which holds our offscreen framebuffer through the following code:

      hMemDC = CreateCompatibleDC( dc );
      hTmpBmp = CreateCompatibleBitmap( dc,ImageWidth, ImageHeight );
      memset( &BitmapInfo.bmiHeader, 0,sizeof( BITMAPINFOHEADER ) );
      BitmapInfo.bmiHeader.biSize = sizeof( BITMAPINFOHEADER );
      BitmapInfo.bmiHeader.biWidth = ImageWidth;
      BitmapInfo.bmiHeader.biHeight = ImageHeight;
      BitmapInfo.bmiHeader.biPlanes = 1;
      BitmapInfo.bmiHeader.biBitCount = 32;
      BitmapInfo.bmiHeader.biSizeImage = ImageWidth*ImageHeight*4;
      UpdateWindow( hWnd );
  5. After the application's window is created, we have to run a typical message loop:

      MSG msg;
      while ( GetMessage( &msg, NULL, 0, 0 ) )
      {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
      }
      …
    }
  6. This program only handles the window destruction event and does not render anything. Compilation of this program is done with a single command as follows:

    >gcc -o main.exe main.c -lgdi32

How it works…

To render a framebuffer on the screen, we need to create a so-called device context with an associated bitmap, and add the WM_PAINT event handler to the window function.

To handle the keyboard and mouse events, we add the WM_KEYUP and WM_MOUSEMOVE cases to the switch statement in the previous program. Actual event handling is performed in the externally provided routines OnKeyUp() and OnMouseMove(), which contain our game logic.

The following is the complete source code of the program (some omitted parts, similar to the previous example, are omitted). The functions OnMouseMove(), OnMouseDown(), and OnMouseUp() accept two integer arguments that store the current coordinates of the mouse pointer. The functions OnKeyUp() and OnKeyDown() accept a single argument—the pressed (or released) key code:

#include <windows.h>

HDC hMemDC;
HBITMAP hTmpBmp;
BITMAPINFO BmpInfo;

In the following code, we store our global RGBA framebuffer:

unsigned char* g_FrameBuffer;

We do all OS-independent frame rendering in this callback. We draw a simple XOR pattern (http://lodev.org/cgtutor/xortexture.html) into the framebuffer as follows:

void DrawFrame()
{
  int x, y;
  for (y = 0 ; y < ImageHeight ; y++)
  {
    for (x = 0 ; x < ImageWidth ; x++)
    {
      int Ofs = y * ImageWidth + x;
      int c = (x ^ y) & 0xFF;
      int RGB = (c<<16) | (c<<8) | (c<<0) | 0xFF000000;
      ( ( unsigned int* )g_FrameBuffer )[ Ofs ] =	RGB;
    }
  }
}

The following code shows the WinAPI window function:

LRESULT CALLBACK MyFunc(HWND h, UINT msg, WPARAM w, LPARAM p)
{
  PAINTSTRUCT ps;
  switch(msg)
  {
  case WM_DESTROY:
    PostQuitMessage(0);
break;
  case WM_KEYUP:
    OnKeyUp(w);
break;
  case WM_KEYDOWN:
    OnKeyDown(w);
break;
  case WM_LBUTTONDOWN:
    SetCapture(h);
    OnMouseDown(x, y);
break;
  case WM_MOUSEMOVE:
    OnMouseMove(x, y);
break;
  case WM_LBUTTONUP:
    OnMouseUp(x, y);
    ReleaseCapture();
break;
  case WM_PAINT:
    dc = BeginPaint(h, &ps);
    DrawFrame();         

Transfer the g_FrameBuffer to the bitmap through the following code:

    SetDIBits(hMemDC, hTmpBmp, 0, Height,g_FrameBuffer, &BmpInfo, DIB_RGB_COLORS);
    SelectObject(hMemDC, hTmpBmp);

And copy it to the window surface through the following code:

    BitBlt(dc, 0, 0, Width, Height, hMemDC, 0, 0, SRCCOPY);
    EndPaint(h, &ps);
break;
  }
  return DefWindowProc(h, msg, w, p);
}

Since our project contains a make file the compilation can be done via a single command:

>make all

Running this program should produce the result as shown in the following screenshot, which shows the Win_Min2 example running on Windows:

There's more…

The main difference between the Android and Windows implementation of a main loop can be summarized in the following way. In Windows, we are in control of the main loop. We literally declare a loop, which pulls messages from the system, handles input, updates the game state, and render s the frame (marked green in the following figure). Each stage invokes an appropriate callback from our portable game (denoted with blue color in the following figure). On the contrary, the Android part works entirely differently. The main loop is moved away from the native code and lives inside the Java Activity and GLSurfaceView classes. It invokes the JNI callbacks that we implement in our wrapper native library (shown in red). The native wrapper invokes our portable game callbacks. Let's summarize it in the following way:

The rest of the book is centered on this kind of architecture and the game functionality will be implemented inside these portable On...() callbacks.

There is yet another important note. Responding to timer events to create animation can be done on Windows with the SetTimer() call and the WM_TIMER message handler. We get to that in Chapter 2, Porting Common Libraries, when we speak about rigid body physics simulations. However, it is much better to organize a fixed time-step main loop, which is explained later in the book.

See also

  • Chapter 6, Unifying OpenGL ES 3 and OpenGL 3

  • The recipe Implementing the main loop in Chapter 8, Writing a Match-3 Game

 

Unifying the cross-platform code


Right now, we have two different versions of a simple program (Win_Min2 and App3). Let us see how to unify the common parts of the code.

Getting ready

In Android, the application initialization phase is different, and since we use a mixed Java plus C++ approach, the entry points will be different. In C++, we are tied to, int main() or DWORD WinMain() functions; whereas in Android it is up to us to choose which JNI function we may call from our Java starter code. Event handling and rendering the initialization code are also quite different, too. To do so, we mark sections of the code with pre-processor definitions and put the different OS code into different files—Wrappers_Android.h and Wrappers_Windows.h.

How to do it...

We use the standard macros to detect the OS for which the program is being compiled: Windows-targeted compilers provide the _WIN32 symbol definition, and the __linux__ macro is defined on any Linux-based OS, including Android. However, the __linux__ defination is not enough, since some of the APIs are missing in Android. The macro ANDROID is a non-standard macro and we pass the -DANDROID switch to our compiler to identify the Android target in our C++ code. To make this for every source file, we modify the CFLAGS variable in the Android.mk file.

Finally, when we write the low-level code, the detection looks like the following code:

#if defined(_WIN32)
// windows-specific code
#elif defined(ANDROID)
// android-specific code
#endif

For example, to make an entry point look the same for both the Android and Windows versions, we write the following code:

#if defined(_WIN32)
#  define APP_ENTRY_POINT()  int main()
#elif defined(ANDROID)
#  define APP_ENTRY_POINT() int App_Init()
#endif

Later we will replace the int main() definition with the APP_ENTRY_POINT() macro.

There's more...

To detect more operating systems, compilers, and CPU architectures, it is useful to check out a list of predefined macros at http://predef.sourceforge.net.

 

Linking and source code organization


In the previous recipes, we learned how to create basic wrappers that allow us to run our application on Android and Windows. However, we used an ad-hoc approach since the amount of source code was low and fit into a single file. We have to organize our project source files in a way suitable for building the code for larger projects in Windows and Android.

Getting ready

Recall the folder structure of the App3 project. We have the src and jni folders inside our App2 folder. The jni/Android.mk, jni/Application.mk, and build.xml files specify the Android build process. To enable the Windows executable creation, we add a file named Makefile, which references the main.cpp file.

How to do it...

The following is the content of Makefile:

CC = gcc
all:
  $(CC) -o main.exe main.cpp -lgdi32 -lstdc++

The idea is that when we add more and more OS-independent logic, the code resides in .cpp files, which do not reference any OS-specific headers or libraries. For the first few chapters, this simple framework that delegates frame rendering and event handling to portable OS-independent functions (OnDrawFrame(), OnKeyUp() and so on) is enough.

How it works...

All of our examples from the subsequent chapters are buildable for Windows from the command line using a single make all command. Android native code is buildable with a single ndk-build command. We will use this convention throughout the rest of the book.

 

Signing release Android applications


Now we can create a cross-platform application, debug it on a PC, and deploy it to Android devices. We cannot, however, upload it on Google Play because it is not (yet) signed properly with the release key.

Getting ready

A detailed explanation of the signing procedure on Android is given in the developer manual at http://developer.android.com/tools/publishing/app-signing.html. We will focus on the signing from the command line and automating the entire process via batch files.

How to do it...

First of all, we need to rebuild the project and create a release version of the .apk package. Let's do it with our App2 project:

>ndk-build -B
>ant release

You should see a lot of text output from Ant, which ends with something like the following command:

-release-nosign:
[echo] No key.store and key.alias properties found in build.properties.
[echo] Please sign App2\bin\App2-release-unsigned.apk manually
[echo] and run zipalign from the Android SDK tools.

Let us generate a self-signed release key using keytool from the JDK through the following command:

>keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

Fill out all the fields necessary for the key, as in the following command:

Enter keystore password:
Re-enter new password:
What is your first and last name?
  [Unknown]:  Sergey Kosarevsky
What is the name of your organizational unit?
  [Unknown]:  SD
What is the name of your organization?
  [Unknown]:  Linderdaum
What is the name of your City or Locality?
  [Unknown]:  St.Petersburg
What is the name of your State or Province?
  [Unknown]:  Kolpino
What is the two-letter country code for this unit?
  [Unknown]:  RU
Is CN=Sergey Kosarevsky, OU=SD, O=Linderdaum, L=St.Petersburg, ST=Kolpino, C=RU correct?
  [no]:  yes

Generating 2048 bit RSA key pair and self-signed certificate (SHA1withRSA) with a validity of 10000 days
        for: CN=Sergey Kosarevsky, OU=SD, O=Linderdaum, L=St.Petersburg, ST=Kolpino, C=RU
Enter key password for <alias_name>
        (RETURN if same as keystore password):
[Storing my-release-key.keystore]

Now we are ready to proceed with the actual application signing. Use the jarsigner tool from the JDK through the following code:

>jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore my-release-key.keystore bin\App2-release-unsigned.apk alias_name

This command is interactive, and it will require the user to enter the keystore password and the key password. However, we can provide passwords in a batch file in the following way:

>jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore my-release-key.keystore -storepass 123456 –keypass 123456 bin\App2-release-unsigned.apk alias_name

Passwords should match what you entered while creating your release key and keystore.

There is one more step left before we can safely publish our .apk package on Google Play. Android applications can access uncompressed content within .apk using mmap() calls. Yet, mmap() may imply some alignment restrictions on the underlying data. We need to align all uncompressed data within .apk on 4-byte boundaries. Android SDK has the zipalign tool to do it, as seen in the following command:

>zipalign -v 4 bin\App2-release-unsigned.apk App2-release.apk

Now our .apk is ready to be published.

See also

About the Authors

  • Sergey Kosarevsky

    Sergey Kosarevsky is a software engineer with experience in C++ and 3D graphics. He worked for mobile industry companies and was involved in mobile projects at SPB Software, Yandex, Layar and Blippar. He has more than 12 years of software development experience and more than 6 years of Android NDK experience. Sergey got his PhD in the field of mechanical engineering from St. Petersburg Institute of Machine-Building in Saint-Petersburg, Russia. In his spare time, Sergey maintains and develops an open source multiplatform gaming engine, the Linderdaum Engine (http://linderdaum.com).

    Browse publications by this author
  • Viktor Latypov

    Viktor Latypov is a software engineer and mathematician with experience in compiler development, device drivers, robotics, and high-performance computing and with a personal interest in 3D graphics and mobile technology. Surrounded by computers for more than 20 years, he enjoys every bit of developing and designing software for anything with a CPU inside. Viktor holds a PhD in applied mathematics from Saint-Petersburg State University.

    Browse publications by this author

Latest Reviews

(1 reviews total)
very good book. working my way through but well worth buying
Book Title
Access this book and the full library for FREE
Access now