Android游戏开发框架.docx
《Android游戏开发框架.docx》由会员分享,可在线阅读,更多相关《Android游戏开发框架.docx(21页珍藏版)》请在冰豆网上搜索。
Android游戏开发框架
Android游戏开发:
游戏框架的搭建
(1)
通常情况下,游戏开发的基本框架中,一般包括以下模块:
窗口管理(Windowmanagement):
该模块负责在Android平台上创建、运行、暂停、恢复游戏界面等功能。
输入模块(Input):
该模块和视窗管理模块是密切相关的,用来监测追踪用户的输入(比如触摸事件、按键事件、加速计事件等)。
文件输入输出(FileI/O):
此模块用来读取assets文件下图片、音频等资源。
图像模块(Graphics):
在实际游戏开发中,这个模块或许是最复杂的部分。
它负责加载图片并把它们绘制到屏幕上。
音频模块(Audio):
这个模块负责在不同的游戏界面加载音各类频。
网络(networking):
如果游戏提供多人游戏联网功能,此模块就是必须的。
游戏框架(Gameframework):
该模块把以上各种模块整合起来,提供一个易用的框架,来轻松地实现我们的游戏。
下面对每一个模块进行详细的描述。
1.窗口管理
我们可以把游戏的窗口想象成一个可以在它上面绘制内容的画布。
窗口管理模块负责定制窗口、添加各种UI组建、接受各类用户的输入事件。
这些UI组件或许可以通过GPU等硬件加速(比如使用了OpenGLES)。
该模设计时不是提供接口,而是和游戏框架整合在一起,之后会有相关的代码贴出。
我们需要记住的是应用程序状态和窗口事件是该模块必须处理的事情:
Create:
当窗口被创建时被调用的方法。
Pause:
当应用程序由于默写原因暂停时调用的方法。
Resume:
当应用程序恢复到前台时调用的方法。
2.输入模块
大部分操作系统中,输入事件(比如触屏事件、按键事件)是通过当前的窗口调度(dispatched)的,窗口再进一步把这些事件派发给当前选中的组件。
因此我们只需要关注组件的事件即可。
操作系统提供的UIAPIs提供了事件分发机制,我们可以很容易地注册和监听事件,这也是输入模块的主要职责。
有两种处理事件的做法:
轮询(Polling):
在这种机制下,我们仅检查输入设备的当前状态,之前和之后的状态并无保存。
这种输入事件处理适合处理诸如触屏按钮事件,而不适合跟踪文本的输入,因为按键事件的顺序并未保存。
基于事件的处理(Event-basedhandling):
这种机制提供了记忆功能的事件处理,比较适合处理文本输入或者其他需要按键次序的操作。
在Android平台中,主要有三种输入事件:
触屏事件、按键事件和加速计事件,前两种时间使用轮询机制和基于事件处理的机制都适合,加速计事件通常是轮询机制。
触屏事件有三种:
Touchdown:
手机触屏时发生。
Touchdrag:
手指拖动时发生,此前有Touchdown事件产生。
Touchup:
手指抬起时发生。
每种触摸事件有相关的辅助信息:
触屏的位置、指针索引(多点触摸时用来追踪识别不同的触点)
键盘事件包括两种:
Keydown:
按下键盘时触发。
Keyup:
释放键盘时触发。
每种按键事件也有相关的辅助信息:
Key-down事件存储按键码,Key-up事件存储按键码和实际的Unicode字符。
加速计事件,系统不停的轮询加速剂的状态,并以三位坐标标识。
基于以上介绍,下面定义输入模块的一些接口,用来轮询触屏事件、按键事件和加速计事件。
代码如下:
Input.java
packagecom.badlogic.androidgames.framework;
importjava.util.List;
publicinterfaceInput{
publicstaticclassKeyEvent{
publicstaticfinalintKEY_DOWN=0;
publicstaticfinalintKEY_UP=1;
publicinttype;
publicintkeyCode;
publiccharkeyChar;
}
publicstaticclassTouchEvent{
publicstaticfinalintTOUCH_DOWN=0;
publicstaticfinalintTOUCH_UP=1;
publicstaticfinalintTOUCH_DRAGGED=2;
publicinttype;
publicintx,y;
publicintpointer;
}
publicbooleanisKeyPressed(intkeyCode);
publicbooleanisTouchDown(intpointer);
publicintgetTouchX(intpointer);
publicintgetTouchY(intpointer);
publicfloatgetAccelX();
publicfloatgetAccelY();
publicfloatgetAccelZ();
publicListgetKeyEvents();
publicListgetTouchEvents();
}
上述定义包括两个静态类:
KeyEvent和TouchEvent。
KeyEvent类和TouchEvent类都定义了相关事件的常量。
KeyEvent同时定义了几个存储事件信息的变量:
类型(type)、按键码(keyCode)和Unicode字符(keyChar)。
TouchEvent也一样,定义了位置信息(x,y)、某个出触摸点的ID。
比如第一个手指按下,ID是0,第二个手指按下ID是1;如果两个手指按下,手指0释放,手指1保持,又一个手指按下时,ID为已经释放的0.
下面是输入接口中的轮询方法:
Input.isKeyPressed()输入参数是keyCode,返回相应的按键是否按下的布尔值;Input.isTouchDown(),Input.getTouchX()和Input.getTouchY()返回给定指针索引是否按下、对应的横竖坐标值,注意当对应的指针索引不存在时坐标值是未定义的。
Input.getAccelX(),Input.getAccelY(),andInput.getAccelZ()返回各自的加速计的坐标值;最后两个方法用作基于事件处理机制的,他们返回KeyEvent和TouchEvent实例,用于记录上次事件触发的信息,最新的事件在列表的最后。
通过这些简单的接口和类的,我们构建了我们的输入接口。
下节继续分析文件处理的内容(FileI/O)。
3.文件读写(FileI/O)
读写文件在游戏开发中是一项十分重要的功能。
在Java开发中,我们主要关注InputStream和OutputStream及其实例,它们是Java中读写文件的标准方法。
游戏开发中,比较多的是读取资源文件,比如配置文件、图片、音频文件等;写入文件的操作一般在保存用户进度和配置信息时使用。
下面是文件读写的接口:
FileIO.java
packagecom.badlogic.androidgames.framework;
importjava.io.IOException;
importjava.io.InputStream;
importjava.io.OutputStream;
publicinterfaceFileIO{
publicInputStreamreadAsset(StringfileName)throwsIOException;
publicInputStreamreadFile(StringfileName)throwsIOException;
publicOutputStreamwriteFile(StringfileName)throwsIOException;
}
上述代码中,我们传递一个文件名作为参数,返回一个流,具体的执行会在接口的实现中体现。
同时我们会抛出一个IOException异常以防止读写文件出错。
当我们完成读写后,需要关闭输入输出流。
Asset文件从应用程序的APK文件中读取,其他文件一般从内置存储器或者SDCard上读取,分别对应上述代码中的三个方法。
Android游戏开发:
游戏框架的搭建
(2)
4.音频模块(Audio)
音频模块编程从来都是一个复杂的话题。
这里不打算用到一些高级复杂的音频处理手段,主要是播放一些背景音乐。
在书写代码前,让我们了解一下音频的基础知识。
采样率:
定义了每秒从连续信号中提取并组成离散信号的采样个数,采样率越高音质越好,单位用赫兹(Hz)来表示,CD一般是44.1KHz。
对于每个采样系统会分配一定存储位(bit数)来表达声波的声波振幅状态,称之为采样分辨率或采样精度,每增加1个bit,表达声波振幅的状态数就翻一翻,并且增加6db的动态范围态,1个2bit的数码音频系统表达千种状态,即12db的动态范围,以此类推。
如16bit能够表达65536种状态,24bit可以表达多达16777216种状态。
动态范围是指声音从最弱到最强的变化范围,人耳的听觉范围通常是20HZ~20KHZ。
高的采样率意味着更多的存储空间。
比如60s的声音,采样率8KHz、8bits,大约0.5M,采样率44KHz、16bits,超过5M,普通的3分钟的流行歌曲,将会超过15M。
为了即不降低质量有不太占据空间,很多比较好的压缩方法被提出来。
比如MP3s和OGGs格式就是网络中比较流行的压缩格式。
可以看到3min的歌曲占了不少空间。
当我们播放游戏的后台音乐时,我们可以把音频流化而不是预先加载到内存。
通常背景音乐只有一个,因此只需要到磁盘加载一次即可。
对于一些短的音效,比如爆炸声和枪的射击声,情况有所不同。
这些短的音效经常会同时被调用多次,从磁盘对每个实例流化这些音效不是一个好的办法。
幸运的是,短的音效并不占用太多的内存空间,因此只需把这些音效提前读入到内存即可,然后可以直接地同时播放这些音效。
因此我们的代码需要提供如下功能:
我们需要一种方法加载音频文件,用于流化播放(Music)和内存播放(Sound),同时提供控制播放功能。
相应的接口有三个,Audio、Music和Sound,代码如下。
Audio接口Audio.java
packagecom.badlogic.androidgames.framework;
publicinterfaceAudio{
publicMusicnewMusic(Stringfilename);
publicSoundnewSound(Stringfilename);
}
Audio接口创建新的Music和Sound实例。
一个Music实例表示一个流音频文件,一个Sound实例表示一个保存在内存中的短的音效。
方法Audio.newMusic()和Audio.newSound()都是以文件名作为参数并抛出IOException以防文件加载失败(例如文件不存在或者文件损坏等情况)。
Music接口Music.jva
packagecom.badlogic.androidgames.framework;
publicinterfaceMusic{
publicvoidplay();
publicvoidstop();
publicvoidpause();
publicvoidsetLooping(booleanlooping);
publicvoidsetVolume(floatvolume);
publicbooleanisPlaying();
publicbooleanisStopped();
publicbooleanisLooping();
publicvoiddispose();
}
Music接口有点复杂,包含了播放音乐流、暂定和停止、循环播放、音量控制(从0到1的浮点数)方法。
当然,里面还有一些getter方法,用来获取当前音乐实例的状态。
当我们不再需要Music实例时,我们可以销毁它(dispose方法),这会关闭系统资源,即流化的音频文件。
Sound接口Sound.java
packagecom.badlogic.androidgames.framework;
publicinterfaceSound{
publicvoidplay(floatvolume);
publicvoiddispose();
}
Sound接口比较简单,只包含play()和dispose()方法。
前者以指定的音量为输入参数,我们可以在任何我们需要的时候播放音效。
后者在我们不许Sound实例时,我们需要销毁它以释放它占用的内存空间。
Android游戏开发:
游戏框架的搭建(3)
5.图像模块(Graphics)
最后一个模块是图像操作模块,用来绘制图像到屏幕上。
不过要想高性能的绘制图像,就不得不了解一些基本的图像编程知识。
让我们从绘制2D图像开始,首先要了解的一个问题是:
图像究竟是如何绘制到屏幕的?
答案相当复杂,我们不需要知道所有的细节。
光栅、像素和帧缓冲(Framebuffers)
现在的显示器都是基于光栅的,光栅是一个两维度的格子组成,也就是像素格。
光栅格子的长宽,我们一般用像素来表示。
如果仔细观察显示器(或者用放大镜),我们就可以发现显示器上面有一个一个的格子,这就是像素格或者光栅格。
每个像素的位置可以用坐标表示,于是引入了二维坐标系统,这也意味着坐标值是整数。
显示器源源不断地收到从图形处理器传过来的图像流,解码每个像素的颜色(程序或者操作系统设定),然后绘制到屏幕上。
每秒钟显示器会进行多次刷新,刷新频率单位是Hz,比如LCD显示器主流刷新率是85Hz。
图形处理器需要从一个特殊的存储区域获取像素信息以便显示在显示器上,这个区域就叫做视频内存区,或者叫VRAM。
这个区域一般称作帧缓冲区(framebuffer)。
因此一个完整的屏幕图形叫做一个帧。
对于每个显示器栅格中的像素,在帧缓冲区都有一个对应的内存地址。
当我们需要改变屏幕显示内容时,我们只需要简单地改变帧缓冲区中的内容即可。
下图是显示器栅格和帧缓冲区的简单示意图:
垂直同步和双缓冲
普通的绘图方法,当要绘制的对象太复杂,尤其是含有位图时,这时的画面会显示的很慢,对于运动的画面,会给人“卡”住了的感觉,有时候还会导致画面闪烁。
于是我们采用双缓冲技术(采用两个framebuffer)。
双缓冲的原理可以这样形象的理解:
把电脑屏幕看作一块黑板。
首先我们在内存环境中建立一个“虚拟“的黑板,然后在这块黑板上绘制复杂的图形,等图形全部绘制完毕的时候,再一次性的把内存中绘制好的图形“拷贝”到另一块黑板(屏幕)上。
采取这种方法可以提高绘图速度,极大的改善绘图效果。
下面是原理图:
要知道什么是垂直同步,必须要先明白显示器的工作原理。
显示器上的所有图像都是一线一线的扫描上去的,无论是隔行扫描还是逐行扫描,显示器,都有2种同步参数——水平同步和垂直同步。
水平同步信号决定了CRT画出一条横越屏幕线的时间,垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间,而恰恰是垂直同步代表着CRT显示器的刷新率水平!
关闭垂直同步:
我们平时运行操作系统一般屏幕刷新率一般都是在85Hz上下,此时显卡就会每按照85Hz的频率时间来发送一个垂直同步信号,信号和信号的时间间隔是85的分辨率所写一屏图像时间。
打开垂直同步:
在游戏中,或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。
这样fps自然要受到操作系统刷新率运行值的制约。
也就是说,当然打开后如果你的游戏画面FPS数能达到或超过你显示器的刷新率,这时你的游戏画面FPS数被限制为你显示器的刷新率。
如果达不到会出现不同程度的跳帧现象,FPS与刷新率差距越大跳帧越严重。
一般对于高性能的显卡建议打卡,游戏画面会更好!
打开后能防止游戏画面高速移动时画面撕裂现象,比如实况足球等。
关闭垂直同步,那么游戏中作完一屏画面,显卡和显示器无需等待垂直同步信号,就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。
但是,不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面平滑,使得画面稳定。
取消了垂直同步信号,固然可以换来更快的速度,但是在图像的连续性上,性能势必打折扣。
这也是关闭垂直同步后发现画面不连续的理论原因!
图像格式
比较流行的两个图形格式是JPEG和PNG。
JPEG是有损压缩格式,PNG是无损压缩格式,因此PNG格式可以百分百重现原始的图像。
有损压缩格式通常占用少的磁盘空间。
我们采用何总压缩格式取决于我们的磁盘空间。
和音频类似,当我们加载到内存中时,我们需要完全地解压一个图像。
因此,即使你的压缩图像在磁盘上只有20K,在RAM中你依然需要width×height×colordepth的存储空间。
图像叠加
假定有一个我们可以渲染的帧缓冲区(framebuffer),同时有几个加载到RAM中的图片,我们笑需要把RAM中的图片逐次放入到帧缓冲区,比如一个背景图片和一个前景图片如图所示:
这个过程就叫做图像的合成和叠加,我们需要把不同的图片合成一个最终显示的图片。
绘制图片的此项很重要,因为上面的图片总会覆盖下面的图片。
上面图像合成出现了问题:
第二张图片的白色背景覆盖了第一张背景图片。
我们怎样把第二张图的白色背景消去呢?
这就需要alpha混合(alphablending)。
alpha混合是一种把源点的颜色值和目标点的颜色值按照一定的算法进行运算,得到一种透明的效果。
下面是最终合成图像的RGB值,公式如下
?
red=src.red*src.alpha+dst.red*(1–src.alpha)
blue=src.green*src.alpha+dst.green*(1–src.alpha)
green=src.blue*src.alpha+dst.blue*(1–src.alpha)
src和dst分别是我们需要混合的源图像和目标图像(源图像相当于人物,目标图像相当于背景)。
下面是一个例子。
?
src=(1,0.5,0.5),src.alpha=0.5,dst=(0,1,0)
red=1*0.5+0*(1–0.5)=0.5
blue=0.5*0.5+1*(1–0.5)=0.75
red=0.5*0.5+0*(1–0.5)=0.25
效果如下图所示
上述公式用了两次乘法,乘法消耗的时间多,为了提高运算速度,可以进行优化。
如
red=(src.red-dst.red)*src.alpha+dst.red
Alpha是一个浮点数,我们可以转换成整数运算,因为一种颜色最多占8Bit,所以Alpha值最多是256,于是我们把Alpha的值乘以256,然后运算的时候再除以256,就得到下面的公式:
red=(src.red-dst.red)*src.alpha/256+dst.red
这里,Alpha是一个0到256的数值。
具体到这个例子,我们只需要把源文件的白色像素的alpha值设为0即可。
最终效果如下图:
图像模块的接口代码
通过以上介绍,我们可以开始设计我们的图像模块的接口。
问下需要实现如下功能:
∙从磁盘加载图片到内存中,为以后绘制到屏幕做准备。
∙用特定颜色清除framebuffer
∙用指定颜色在framebuffer指定位置绘制像素。
∙在framebuffer上绘制线条和矩形。
∙绘制上面内存中的图片到framebuffer,能够整个绘制和部分绘制,alpha混合绘制。
∙得到framebuffer的长宽。
这里用两个接口来实现:
Graphics和Pixmap,下面是Graphics接口:
packagecom.badlogic.androidgames.framework;
publicinterfaceGraphics{
publicstaticenumPixmapFormat{
ARGB8888,ARGB4444,RGB565
}
publicPixmapnewPixmap(StringfileName,PixmapFormatformat);
publicvoidclear(intcolor);
publicvoiddrawPixel(intx,inty,intcolor);
publicvoiddrawLine(intx,inty,intx2,inty2,intcolor);
publicvoiddrawRect(intx,inty,intwidth,intheight,intcolor);
publicvoiddrawPixmap(Pixmappixmap,intx,inty,intsrcX,intsrcY,intsrcWidth,intsrcHeight);
publicvoiddrawPixmap(Pixmappixmap,intx,inty);
publicintgetWidth();
publicintgetHeight();
}
枚举PixmapFormat保存了该游戏支持的像素的颜色值(包括透明度)。
比如ARGB8888,A表示透明度,R表示红色,G表示绿色,B表示蓝色,他们非别用8位来表示,就是各有256种状态。
接下来看下接口的方法:
∙Graphics.newPixmap()方法加载指定格式的图片。
∙Graphics.clear()方法用特定颜色清除framebuffer。
∙Graphics.drawPixel()方法在framebuffer中指定位置绘制给定颜色的像素。
∙Graphics.drawLine()和Graphics.drawRect()方法在framebuffer绘制线条和矩形。
∙Graphics.drawPixmap()方法绘制图像的到framebuffer。
(x,y)坐标指定了framebuffer绘制的起始位置,参数