Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Terminal Velocity Android

4.47/5 (9 votes)
23 May 2013Apache9 min read 43.7K   977  
An android game using NDK JNI and Java.
  1. SettingEcpliseNdkProject 
  2. Jni Described
  3. Data Flow Control
  4. Code Snippet
  5. Game on x86 emulator
  6. Native debugging on emulator

Introduction

Terminal Velocity is a fast action game. It is one of those games where you can play the game with one hand ,The main desire behind creating this was, creating a game which is can be played single handed,and need fast response. I was thinking of some space related while designing this like,space elevator a gas giant , etc so this came out finally .

Hope it will fun while you dodge current and get pulled by emf of retro magnets on side .......

Image 1Image 2

Description

In terminal velocity you control a plane using accelerometer (arrow keys on emulator ) ,flying fast and at the same time escaping from collision with force fields and collecting all the batteries that power up your planes engine and speed up the game for a short period.. It have a leader board record local high score, global score services like score-loop can be integrated easily .

Image 3Image 4

Technical Information

This game is a mixed project half of it is in C++ ,and rest is in Java, using JNI calls as bridge between Java and C++, before we discuss further about technical specs of this game ,this article will use references to JNI .NDK debugging and thus making a native android game .

Steps to start an NDK project

First of all setting environment for a native app.

Open a eclipse workspace . (Assuming this is a new workspace ) we need to set up ndk path include files and ndk debug variable

  1. Create a new android app from file menu. file->new->Android Application Project

    Image 5

  2. To add NDK path to workspace click windows-> preference->android browse for ndk location

    Image 6

  3. Add native support to android app in android app contextmenu->android tools->add native support it will open a dialog asking for native library name

    Image 7

  4. Now you can see header files not resolved in your project .add header files via

    Project->Properties->C/C++ General Includes browse for include dir inside ndk folder.

    Image 8

  5. With all above set add NDK_DEBUG=1 via Project->Properties->C/C++ Build
  6. Now code, set a breakpoint in JNI call or native code. debug app as native app it is also a option in app context menu ,as debugger took a while to settle to just debug anything that is just with app start I will suggest please open your app in debugging quit app and reopen it or use some time based delay in oncreate method.

Now let us discuss Jni the sole of my project .

As calling onload in a static part will load the library as soon as that class is loaded . If you are not implementing onload in .so library it will produce a warning ignore it .

Loading jni library

Java
static{System.loadLibrary("engine");} //the same name we had placed in input box in step 3 . 

To call a function from java to c we only need to define and native and its c/c++ implementation

example: java declaration

C++
package inductionlabs.jni.bridge;
public static native int object(Object o,int i);     

C++ definitions : of same only our function name would be

JNIEXPORT returntype JNICALL Java_packagename_filename_functionname ,

Dot is replaced by underscore, first parameter is jni pointer , second is class ,third and so one your passed parameters.

C++
extern "C"
{
JNIEXPORT int JNICALL Java_inductionlabs_jni_bridge_Bridge_object(JNIEnv * env, jclass class,jobject obj,jsize i)
{ 
engine::Glgame=env->NewGlobalRef(obj);break;
engine::setting=env->FindClass("inductionlabs/jni/bridge/tools_seting_bridge");
engine::setting=(jclass)(env->NewGlobalRef(engine::setting));
}
return i;
}   
This is quite easier to do Vs doing the opposite i.e. calling a java function from c++ we need its class, method signature,and a object if it is not static. after ics android refer jni objects as weak objects ,i.e they loose their handles so no guarantee while using them and all the time it will crash your app. We need to get the class object and make it global as follows for calling any static functions
Java
 engine::setting=env->FindClass("inductionlabs/jni/bridge/tools_seting_bridge");
engine::setting=(jclass)(env->NewGlobalRef(engine::setting));   

In first line we got the class reference and then created a new global reference now we will use this and object to call back java function

Java
 static void adjustVolume(JNIEnv *g_env,jfloat vol,int channel,const char * path)
{
    jmethodID mid= g_env->GetMethodID(javaBatcher,"adjustvolume","(FILjava/lang/String;)I");
    jstring name = g_env->NewStringUTF(path);
    int l=g_env->CallIntMethod(Batcher, mid,vol,channel,name);
    return;
}

We can create any method signature as using following simple rule

signature will be (Parameters)returntype, where we will use

Z for boolean,B for byte,C for char,S for short,I for int,J for long,Ffor float,D for double.

any object is written with its whole class name preceded by L and followed by ;
example String is Ljava/lang/String;

and array are written as [array like [I is a int array
so a class having two strings and 1 int and returning void would have signature as

"(ILjava/lang/String;I)V" 

Some fixes required by every accelerometer game .

The Game managing the axes of accelerometer .

As this is an accelerometer game I would want to discuss one more thing, android devices not only come in portrait default mode but exactly can even be landscaped default So the axis among different device work differently

here is solution: Fix a choice of lock lock your app in portrait or landscape mode.

Know the rotation by calling following code

Java
Display display = ((WindowManager)contex.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
orientation= display.getRotation();

fix it by calling following.

Java
 switch(TerVel.orientation)
{case 0:updateacc(-1*accelX,-1*accelY,accelZ);break;
 case 1:    updateacc(1*accelY,-1*accelX,accelZ);break;
 case 2:    updateacc(1*accelX,1*accelY,accelZ);break;
 case 3:    updateacc(-1*accelY,1*accelX,accelZ);break;
 }

Till this point we had discussed all point needed to create a native app . My game is mixed project c/c++ and java not pure ndk activity as now provided by android

with android tools v20 it come with native application development ,so it a bit easy to set up environment usually you only need to do it once.

  1. Make sure you have a Intel pc (I dont have one ) if you want to develop on windows
    no hardware acceleration on amd processor but as checkjni is on in emulator it helped in collection some bugs I was never wondering about.
  2.  Profiler debugger for java and c++ . As both can't be debug simultaneously .(A visual studio plugin is available for this purpose(but both cost money (visual studio pro & visual gdb ))
  3. It would be great if you can have enabled check jni on your device as any error in c will provide segmant faults and a code no explanation . Emulator have this enabled by default.
  4. Lots of patience

My game was scheduled to be completed by now but will take two more days all due to NDK debugging on slow emulator even my device is not working with checkjni

Discussing Game control flow.

Image 9

the above image is created in photoshop describe nearly the control flow of my game .

Some imp Java classes in my game and their work.

  1. TerVel :entry point of app ,Its main function is to load "libengine.so",Assets,Setting and transfer control to glrenderrer.
  2. Assets:It calls LoaderParser ,and after that load all data,control sound and music too
  3. LoaderParser: A class entirely written to parse all game related data ,.pack file by texture packer tool
  4. NativeFun:Class contains all native function definition.
  5. BatcherBridge:Class with all function which get called from c code
  6. Bridge:only used to register objects in c++ as jclass and jobjects .

C++ classes and their work.

  1. Engine: Class used to talk back to java holds all function like sprite draw batch begin batch end set color etc.
  2. Game: This is main game class and holds objects of below classes
  3. GameData : game data to store all data This class always used throughout game to store and retrieve data it ease use while collecting all data
  4. RegionData:stores data per region as game generates all region randomly so providing hours of fun .

some imp c++ files

items.h: Draw all items on screen hero/pickups/enemy/walls

gui.h: Draw gui on screen

input.h: Get all user data but for now, this version of game process all touch data on gui draw call . Only input data from keyboard ("For emulator display ")and accelerometer processed here

jnifun.h :Contains all the definition of native function declared in native fun

Some Code:

Code for Random Level Generation . This is called for generating a part of level

C++
void Game::genregion(int i)
{
  int k=0;
  while(k<gd.maxcoins)
  {
      gd.coinarrayx[k]= (k%7)*40+30;
      gd.coinarrayy[k]=(k/7)*45;
      int a= rand()%10;
      if(gd.coinarrayx[k]>310||gd.coinarrayx[k]<50||a<7)
       { gd.coinarrayx[k]-=600;
       }
       //gd.coinarrayy[k]=(k/8-2)*90;
       k++;
  }k=0;
  while(k<gd.maxenemey)
  {
     gd.enemyx[k]=(k*134)%210;
     gd.enemyx[k]+=55;
     int a= (rand()+rand()+rand()+rand())%100;
     if(gd.enemyx[k]>310||gd.enemyx[k]<50||a<15)
        {   gd.enemyx[k]=2500;
        }
     int ja[]={45,-60,90,-45,30,45,-64,30,-23};
     gd.enemyangle[k]=ja[rand()%9];
     gd.enemytype[k]=rand()%4;
     ////////////////////////////rechance to get type 2 enemey////////////////////////
     if(gd.enemytype[k]==0||gd.enemytype[k]==1)
     gd.enemytype[k]=rand()%4;
     int b[]={85,60,90,100,80,118};
     gd.enemylength[k]=b[rand()%6];
     if(gd.enemytype[k]==0||gd.enemytype[k]==1)
     gd.enemyy[k]=(k-10)*80;
     else
     gd.enemyy[k]=(k-10)*150;
     k++;
     if(gd.enemyy[k]+r1.regiony>-100&&gd.enemyy[k]+r1.regiony<250)
     {gd.enemyy[k]+=gd.herox+450-r1.regiony;
     }
  }
}

Image 10Image 11

Like the difference in position of current batteries emf generators etc.

Code to Parse files

Java
:Packs
 bg
cur
hero
items
over
strike
:Fonts
f
:Music
m1.mp3
:Sound
coin.ogg
select.ogg
cur.ogg
:Packs
stf
gui
jk
   
private static void parse(FileIO files, BufferedReader bf) 
{ 
  String line; 
try 
{
    line = bf.readLine();
    
 int index=0;
  while (line != null) 
  {  if(line.equals(":Packs"))
          {line = bf.readLine();
         index=1;
          }
     else if(line.equals(":Fonts"))
      {line = bf.readLine();
      index=2;
      }
      else if(line.equals(":Music"))
      {line = bf.readLine();
      index=3;
      }
      else if(line.equals(":Sound"))
      {line = bf.readLine();
       index=4;
      }
      else if(line.equals(":patt"))
      {line = bf.readLine();
       index=5;
      }
     else 
     { 
      switch(index)
      {case 1:packloader(files,line+".pack");  Assets.loaderp=10;break;
       case 2:fontloader(files,line);Assets.loaderp=20;break;
       case 3:musicloader(files,line);Assets.loaderp=30;break;
       case 4:soundloader(files,line);Assets.loaderp=40;break;
       case 5:patternloader(files,line);Assets.loaderp=50;break;
      }
      line = bf.readLine();
  }
  }
} catch (IOException e) 
{
    // TODO Auto-generated catch block
    e.printStackTrace();
}
}
private static void patternloader(FileIO files, String line)
{
     
    
}
private static void soundloader(FileIO files, String file) 
{
     Assets.SoundNames.add(file);
     Assets.soundcount++;
    
}
private static void musicloader(FileIO files, String file) 
{
     Assets.MusicNames.add(file);
     Assets.MusicCount++;
    
    // TODO Auto-generated method stub
    
}
private static void fontloader(FileIO files, String file) 
{
 Assets.FontNames.add(file+".png");
 packloader(files,file+".pack");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////.....parse the .pack file and load its data .../////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private static void packloader(FileIO files, String file)
{
InputStreamReader in=null;
BufferedReader bf=null;
try 
{
in = new InputStreamReader(files.readAsset(file));
bf=new BufferedReader(in);
parsepack(files,bf);
bf.close();
}
catch (IOException e) 
{
} catch (NumberFormatException e) {
// :/ It's ok, defaults save our day
} finally {
try {
    if (in != null)
        in.close();
    if (bf != null)
        bf.close();
    
} catch (IOException e) {
}
}
    
}
private static void parsepack(FileIO files, BufferedReader bf) 
{
      
     
    @SuppressWarnings("unused")
    String line= null ,texturename = null,texturegionname= null,format= null,
      filter= null,filter1= null,repeat= null,sub= null,sub1= null,sub2= null;
    @SuppressWarnings("unused")
    Boolean rotate=false;
    int x=0,y=0,sizex=0,sizey=0,orizx=0,orizy = 0,offsetx=0,offsety=0,index=0;
    String [] tokens={".png","format:","filter:","repeat:",
      "rotate:","xy:","size:","orig:","offset:","index:"};
    StringTokenizer st=null;
    try 
     {
     line= bf.readLine();
     while (line != null) 
     {
     if(line.equals(""))
     line=bf.readLine();         
      st=new StringTokenizer(line,COLON);
      
       int i=0;
       while(i<tokens.length)
       { if(line.contains(tokens[i]))    
         break; 
         else
         i++;
       }
       if(line.equals("")){i++;}
       else if(st.countTokens()!=0)
       {if(line.indexOf(":")!=-1)
         { sub=line.substring(line.indexOf(":")+2);
           if(line.indexOf(",")!=-1)
           {sub1=sub.substring(0,sub.indexOf(","));
           sub2=sub.substring(sub.indexOf(",")+2);
          }
         }
       }
       switch(i)
       {case 0:texturename=line;break;
        case 1:format=sub;break;
        case 2:filter=sub1;filter1=sub2;break;
        case 3:repeat=sub;break;
        case 4:rotate=Boolean.parseBoolean(sub);break;
        case 5:x=Integer.parseInt(sub1);y=Integer.parseInt(sub2);break;
        case 6:sizex=Integer.parseInt(sub1);sizey=Integer.parseInt(sub2);break;
        case 7:orizx=Integer.parseInt(sub1);orizy=Integer.parseInt(sub2);break;
        case 8:offsetx=Integer.parseInt(sub1);offsety=Integer.parseInt(sub2);break;
        case 9:index=Integer.parseInt(sub);break;
        case 10:texturegionname=line;break;
        default:break;
      }
       if(i==3)
       {
           Assets.TextureNames.add(texturename);
           Assets.texcount++;
       }
       if(i==9)
       {   if(texturegionname.equals("ghost"))
             {parseghost(texturename,texturegionname,x,y,sizex,sizey,orizx,orizy,offsetx,offsety,index);
             }
           else
           { texturenameinfo tem=new texturenameinfo(texturename,
                texturegionname,x,y,sizex,sizey,orizx,orizy,offsetx,offsety,index);
             if(Assets.TextureRegionNames==null)Assets.TextureRegionNames=new TexturRegionName(); 
             Assets.TextureRegionNames.add(tem);
             Assets.texregcount++;
           }
       }
       
     line=bf.readLine();
     
     }
     
     } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
     }

Debugging Native Code on x86 system image

To make any android app compatible with different systems we need to build our app library

with different architecture as one for each of these x86 ,arm,armv7,mips etc.

Google play now supports different apk for different platform but we can create a single app with all the available architecture . It has only side effect if you want your app to be small in size and your library is big .

Many developers in market provide a small library in their app and download your platform specific library after words . Like opencv apps.

How to compile for different architectures

setting APP_ABI variable in application.mk to all compiles for all ,where as we can compile for selective by writing space seprated names or writing names using+= opertor etc

this is the line from this game application.mk it s library is getting compiles for x86 and arm

APP_ABI :=x86 armeabi

Image 12Image 13

So the game has been compiled for a x86 system Hurray. The CDT genrated some warning as i include .h files to compile in application.mk but that dosent make a difference .

GameOnEmulator

Now the final part showing how it worked on emulator and how It showed native debugging.

As my Pc is an AMD based pc and I am running windows HAXM is not a choice for me Image 14

Game playing on emulator .

http://www.youtube.com/watch?v=L-3mWsakIY4

NativeDebugginOnEmulator

As checkjni is powerfull feature of emulator .It is handy to use emulator for a segment error ,As either you go all across your code searching for a out of bond error or static calls from static function or can use emulator and it will tell you about the error .

Here is the video link

http://www.youtube.com/watch?v=ISIW_K4I-Oo

Using The Code

A game needs input output some file handling audio even internet(sockets ) .

Our game is a 2d game we designed it across an old version of libgdx pretty much engineered for our need and as the version we used is in java so a lot of jni calls ,so we have to helper classes

Engine in c++ to send our calls to java and a Native Fun class in java to do vice versa .It is always easy to make all of jni code collection under one class (easy to debug)

The code can be easily stripped and used to create a new game from scratch allready one day and I have started coding one more game on same back end.

  1. just write all the correct path for .pack , music,audio,pattern, in assets.item and it will import all of those texture ,texture regions for you just
  2. put a watch and get the order of textureregion and texture.
  3. Create enum in c++ by copying the data from watch.And you are just ready to create your own next adventure game.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0