- SettingEcpliseNdkProject
- Jni Described
- Data Flow Control
- Code Snippet
- Game on x86 emulator
- 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 .......
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 .
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 .
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
- Create a new android app from file menu. file->new->Android Application Project
- To add NDK path to workspace click windows-> preference->android browse for ndk location
- 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
- 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.
- With all above set add NDK_DEBUG=1 via Project->Properties->C/C++ Build
- 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
static{System.loadLibrary("engine");}
To call a function from java to c we only need to define and native and its c/c++ implementation
example: java declaration
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.
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
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
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
Display display = ((WindowManager)contex.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
orientation= display.getRotation();
fix it by calling following.
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.
- 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. - 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 ))
- 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.
- 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
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.
- TerVel :entry point of app ,Its main function is to load "libengine.so",Assets,Setting and transfer control to glrenderrer.
- Assets:It calls LoaderParser ,and after that load all data,control sound and music too
- LoaderParser: A class entirely written to parse all game related data ,.pack file by texture packer tool
- NativeFun:Class contains all native function definition.
- BatcherBridge:Class with all function which get called from c code
- Bridge:only used to register objects in c++ as jclass and jobjects .
C++ classes and their work.
- Engine: Class used to talk back to java holds all function like sprite draw batch begin batch end set color etc.
- Game: This is main game class and holds objects of below classes
- 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
- 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
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;
}
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;
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;
}
}
}
Like the difference in position of current batteries emf generators etc.
Code to Parse files
: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)
{
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++;
}
private static void fontloader(FileIO files, String file)
{
Assets.FontNames.add(file+".png");
packloader(files,file+".pack");
}
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) {
} 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) {
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
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 .
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
Game playing on emulator .
http://www.youtube.com/watch?v=L-3mWsakIY4
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.
- 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
- put a watch and get the order of textureregion and texture.
- Create enum in c++ by copying the data from watch.And you are just ready to create your own next adventure game.