终端速度 Android






4.47/5 (9投票s)
一款使用NDK JNI和Java开发的Android游戏。
引言
Terminal Velocity 是一款快节奏的动作游戏。这是一款你可以单手玩的游戏,创作它的主要愿望是制作一款可以单手玩、需要快速响应的游戏。在设计时,我曾考虑过一些与太空相关的内容,比如太空电梯、气态巨行星等,最终就形成了这款游戏。
希望你在躲避电流并被侧面复古磁铁的电磁场吸住时,会觉得很有趣……


描述
在 Terminal Velocity 中,你通过加速计(模拟器上的箭头键)控制一架飞机,高速飞行,同时躲避与力场的碰撞,并收集为飞机引擎供电的所有电池,从而在短时间内加快游戏速度。它拥有本地高分排行榜和全球分数服务,如 score-loop 可以轻松集成。
技术信息
这款游戏是一个混合项目,一半用 C++ 编写,另一半用 Java 编写,使用 JNI 调用作为 Java 和 C++ 之间的桥梁。在我们进一步讨论这款游戏的具体技术规格之前,本文将引用 JNI、NDK 调试,从而制作一款原生的 Android 游戏。
开始 NDK 项目的步骤
打开一个 Eclipse 工作区。(假设这是一个新的工作区)我们需要设置 ndk 路径、包含文件和 ndk 调试变量。
- 从文件菜单创建一个新的 Android 应用。 文件 -> 新建 -> Android Application Project
- 要将 NDK 路径添加到工作区,请点击 窗口 -> 首选项 -> Android,然后浏览 NDK 位置。
- 在 Android 应用的上下文菜单中添加原生支持 -> Android 工具 -> 添加原生支持。这会打开一个对话框,询问原生库的名称。
- 现在,您可以看到项目中未解析的头文件。通过以下方式添加头文件:
项目 -> 属性 -> C/C++ General -> Includes,然后浏览 ndk 文件夹内的 include 目录。
- 设置好以上所有内容后,通过 项目 -> 属性 -> C/C++ Build 添加 NDK_DEBUG=1。
- 现在可以编写代码了,在 JNI 调用或原生代码中设置断点。将应用作为原生应用调试,这也是应用上下文菜单中的一个选项。由于调试器需要一段时间才能稳定下来,我建议在应用启动时,先打开应用,然后退出再重新打开,或者在 onCreate 方法中使用一些基于时间的延迟来调试任何内容。
作为静态部分中的 onload 调用,它会在该类加载时立即加载库。如果您没有在 .so 库中实现 onload,会产生一个警告,可以忽略它。
加载 JNI 库
static{System.loadLibrary("engine");} //the same name we had placed in input box in step 3 .
要从 Java 调用 C 函数,我们只需要定义一个 native 函数及其 C/C++ 实现。
示例:Java 声明
package inductionlabs.jni.bridge;
public static native int object(Object o,int i);
C++ 定义:与 Java 声明的函数名相同,只是我们的函数名会是:
JNIEXPORT returntype JNICALL Java_packagename_filename_functionname ,
点被替换为下划线,第一个参数是 jni 指针,第二个是类,第三个及之后的参数是传递的参数。
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;
}
这比反过来调用 C++ 中的 Java 函数要容易得多。调用 Java 函数需要其类、方法签名,以及一个对象(如果不是静态的)。在 ICS 之后,Android 将 JNI 对象视为弱对象,即它们会丢失句柄,因此在使用它们时没有保证,并且总是会崩溃您的应用程序。我们需要获取类对象并将其全局化,如下所示,以调用任何静态函数。 engine::setting=env->FindClass("inductionlabs/jni/bridge/tools_seting_bridge");
engine::setting=(jclass)(env->NewGlobalRef(engine::setting));
第一行获取了类引用,然后创建了一个新的全局引用。然后,我们将使用这个引用和对象来回调 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;
}
我们可以通过以下简单规则创建任何方法签名:
签名将是 (参数)返回值,其中我们将使用:
Z 代表 boolean,B 代表 byte,C 代表 char,S 代表 short,I 代表 int,J 代表 long,F 代表 float,D 代表 double。
任何对象都写成其完整的类名,前面加上 L,后面加上 ;
例如 String 是 Ljava/lang/String;
数组写成 [数组,如 [I 是一个 int 数组。
所以一个包含两个 String 和 1 个 int 并返回 void 的类将具有签名:
"(ILjava/lang/String;I)V"
每个加速计游戏都需要的一些修复。
管理加速计轴的游戏。
由于这是一款加速计游戏,我想再讨论一件事,Android 设备不仅默认是纵向模式,也可以默认横向。因此,不同设备上的轴工作方式不同。
解决方案如下:选择锁定模式,将您的应用锁定在纵向或横向模式。
通过调用以下代码了解旋转情况。
Display display = ((WindowManager)contex.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
orientation= display.getRotation();
通过调用以下代码进行修复。
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;
}
到目前为止,我们已经讨论了创建原生应用所需的所有要点。我的游戏是 C/C++ 和 Java 的混合项目,而不是像 Android 现在提供的纯 NDK 活动。
随着 Android Tools v20 的发布,它支持原生应用程序开发,因此设置环境会更容易一些,通常只需要做一次。
- 如果您想在 Windows 上开发,请确保您有一台 Intel PC(我没有)。
AMD 处理器上没有硬件加速,但由于模拟器上启用了 checkjni,这有助于收集一些我从未想过的 bug。 - 用于 Java 和 C++ 的性能剖析器调试器。两者不能同时进行调试。(有一个 Visual Studio 插件可用于此目的(但两者都需要付费(Visual Studio Pro & Visual GDB)))。
- 如果您可以在设备上启用 check jni,那将是很好的,因为 C 中的任何错误都会导致段错误,而代码没有任何解释。模拟器默认启用了此功能。
- 大量的耐心
我的游戏原计划在此日期前完成,但由于在慢速模拟器上进行 NDK 调试,还需要两天时间,即使我的设备也无法在 checkjni 上正常工作。
讨论游戏控制流程。
上图是在 Photoshop 中绘制的,大致描述了我游戏的控制流程。
我游戏中的一些重要 Java 类及其作用。
- TerVel:应用的入口点。它的主要功能是加载 "libengine.so"、资源,进行设置,并将控制权转移给 glrenderer。
- Assets:它调用 LoaderParser,然后加载所有数据,并控制声音和音乐。
- LoaderParser:一个完全用于解析所有游戏相关数据的类,使用 Texture Packer 工具解析 .pack 文件。
- NativeFun:类包含所有原生函数定义。
- BatcherBridge:包含所有从 C 代码调用的函数的类。
- Bridge:仅用于在 C++ 中注册 jclass 和 jobject 对象。
C++ 类及其作用。
- Engine:用于回退到 Java 的类,包含所有函数,如 sprite draw batch begin batch end set color 等。
- Game:这是主游戏类,包含以下类的对象。
- GameData:游戏数据,用于存储所有数据。该类在整个游戏中始终用于存储和检索数据,方便收集所有数据。
- RegionData:存储每个区域的数据,因为游戏随机生成所有区域,提供数小时的乐趣。
一些重要的 C++ 文件。
items.h:绘制屏幕上的所有物品,包括英雄/拾取物/敌人/墙壁。
gui.h:在屏幕上绘制 GUI。
input.h:获取所有用户数据,但目前,此版本的游戏在 GUI 绘制调用中处理所有触摸数据。此处仅处理来自键盘(“用于模拟器显示”)和加速计的输入数据。
jnifun.h:包含 NativeFun 中声明的所有原生函数的定义。
用于随机关卡生成的代码。这是生成关卡一部分的代码。
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;
}
}
}
例如,当前电池、EMF 发电机等位置的差异。
用于解析文件的代码。
: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();
}
要使任何 Android 应用与不同系统兼容,我们需要构建我们的应用库。
使用不同的架构,为 x86、arm、armv7、mips 等分别构建。
Google Play 现在支持为不同平台提供不同的 APK,但我们可以创建一个包含所有可用架构的单一应用。这样做只有一个副作用,就是如果想减小应用大小,而您的库文件很大。
市场上的许多开发者会在应用中提供一个小型库,然后下载平台特定的库。例如 OpenCV 应用。
如何为不同的架构编译
在 application.mk 中设置 APP_ABI 变量为 all,则会编译所有架构;而我们可以通过编写空格分隔的名称或使用 += 操作符编写名称来编译特定架构。
这是此游戏 application.mk 中的一行,它的库正在为 x86 和 arm 编译。
APP_ABI :=x86 armeabi
所以游戏已经为 x86 系统编译好了,太棒了!CDT 生成了一些警告,因为我在 application.mk 中包含了 .h 文件进行编译,但这并没有造成影响。
模拟器上的游戏
现在,最后一部分将展示它在模拟器上的工作情况以及如何进行原生调试。
由于我的 PC 是 AMD 处理器,并且我运行的是 Windows,HAXM 对我来说不是一个选择。
在模拟器上玩游戏。
http://www.youtube.com/watch?v=L-3mWsakIY4
模拟器上的原生调试
由于 checkjni 是模拟器的强大功能,因此在使用模拟器进行段错误调试时非常方便。您可以搜索整个代码以查找越界错误或来自静态函数的静态调用,或者可以使用模拟器,它会告诉您错误。
这是视频链接:
http://www.youtube.com/watch?v=ISIW_K4I-Oo
使用代码
游戏需要输入输出、文件处理、音频甚至互联网(套接字)。
我们的游戏是一款 2D 游戏,我们基于一个旧版本的 libgdx 设计的,它非常适合我们的需求。由于我们使用的版本是 Java 编写的,因此有很多 JNI 调用,所以我们有两个辅助类。
C++ 中的 Engine 类用于将我们的调用发送到 Java,Java 中的 NativeFun 类用于反之。始终很容易将所有 JNI 代码集合在一个类中(便于调试)。
代码可以轻松地被剥离并用于从头开始创建新游戏。我已经开始在同一个后端编写另一个游戏了。
- 只需在 assets.item 中为 .pack、音乐、音频、纹理等写下所有正确的路径,它就会为您导入所有纹理、纹理区域,只需
- 放置一个监视器,获取纹理区域和纹理的顺序。
- 通过复制监视器中的数据在 C++ 中创建枚举。然后您就可以开始创建自己的下一个冒险游戏了。