图片-小白之家
图片-小白之家
图片-小白之家
图片-小白之家

android游戏开发(简单小游戏开发入门)

通常,游戏开发的基本框架一般包括以下模块:

窗口管理:该模块负责安卓平台上游戏界面的创建、运行、暂停、恢复等功能。

输入模块:该模块与窗口管理模块密切相关,用于监控和跟踪用户的输入(如触摸事件、按钮事件、加速度计事件等)。).

文件输入输出(FileI/O):该模块用于读取资产文件下的图片、音频等资源。

图形:在实际的游戏开发中,这个模块可能是最复杂的部分。它负责加载图片并在屏幕上绘制。

音频模块:这个模块负责在不同的游戏界面加载各种音频。

联网:如果游戏提供多人联网,这个模块是必须的。

游戏框架:这个模块集成了以上模块,提供了一个易用的框架,轻松实现我们的游戏。
图片[1]-android游戏开发(简单小游戏开发入门)-小白之家

下面详细描述了每个模块。

1.窗口管理

我们可以把游戏窗口想象成一个画布,我们可以在上面画内容。窗口管理模块负责定制窗口,添加各种UI组件,接受各种用户的输入事件。这些UI组件可能会被硬件加速,比如GPU(比如OpenGLES)。

该模块不是为了提供接口而设计的,而是与游戏框架集成在一起,然后会发布相关代码。我们需要记住,应用程序状态和窗口事件是本模块必须处理的事情:

创建:创建窗口时调用的方法。

Pause:应用程序因听写而暂停时调用的方法。

恢复:应用程序恢复到前台时调用的方法。

2.输入模块

在大多数操作系统中,输入事件(例如触摸屏事件和按键事件)是通过当前窗口分派的,并且该窗口进一步将这些事件分派给当前选择的组件。因此,我们只需要关注组件的事件。系统提供的UIAPI提供了事件分发机制,我们可以轻松注册和监控事件,这也是输入模块的主要职责。处理事件有两种方法:

轮询:在这个机制中,我们只检查输入设备的当前状态,不保存之前和之后的状态。这种输入事件处理适用于处理触摸屏按钮等事件,但不适用于跟踪文本的输入,因为键事件的顺序没有保存。

基于事件的处理:该机制提供记忆功能的事件处理,更适合处理文本输入或其他需要按键顺序的操作。

在安卓平台中,主要有三种输入事件:触摸屏事件、按钮事件和加速度计事件。前两种适合轮询机制和基于事件的处理机制,加速度计事件通常是轮询机制。

触摸屏事件有三种类型:

触摸:当手机触摸屏幕时发生。

触摸拖动:当手指拖动时发生,并且之前有一个触摸下降事件。

触摸:手指抬起时发生。

每个触摸事件都有相关的辅助信息:触摸屏的位置、指针索引(用于在多次触摸期间跟踪和识别不同的触点)

有两种键盘事件:

按键:按下键盘时触发。

向上:释放键盘时触发。

每个按键事件也有相关的辅助信息:按键事件存储按键代码,按键事件存储按键代码和实际的Unicode字符。

加速度计事件,系统不断轮询加速器的状态,并用三个坐标来标识。

基于以上介绍,下面定义了输入模块的一些接口,用于轮询触摸屏事件、按键事件和加速度计事件。代码如下:

Input.java

packagecom.badlogic.androidgames.framework;

导入Java.util.list;

公共接口输入{

公共静态类KeyEvent{

公共静态最终intKEY_DOWN=0;

公共静态最终intKEY_UP=1;

公共int类型;

publicintkeyCode

公共charkeyChar

}

公共静态类TouchEvent{

公共静态最终intTOUCH_DOWN=0;

公共静态最终intTOUCH_UP=1;

publicstaticfinalintTOUCH_draged=2;

公共int类型;

publicintx,y;

公共int指针;

}

公共布尔IsKeypress(intKeyCode);

公共布尔isTouchDown(int指针);

publicintgetTouchX(int指针);

publicintgetTouchY(int指针);

publicfloatGetAccelx();

publicfloatGetaccelly();

publicfloatGetAccelz();

publicListgetkeyevents();

publicListgetTouchEvents();

}

上面的定义包括两个静态类:KeyEvent和TouchEvent。KeyEvent类和TouchEvent类都定义了相关事件的常数。KeyEvent还定义了几个用于存储事件信息的变量:类型、键码和Unicode字符。触摸事件也定义了触摸点的位置信息(x,y)和标识。比如第一个手指按下时,ID为0,第二个手指按下时,ID为1;如果按下两个手指,释放手指0,握住手指1,当按下另一个手指时,释放ID0。

以下是输入界面中的轮询方式:Input.isKeyPressed()输入参数为keyCode,返回对应键是否被按下的布尔值;Input.isTouchDown(),输入。Getouchx()和Input.getTouchY()返回给定指针索引是否被按下,以及相应的水平和垂直坐标值。请注意,当相应的指针索引不存在时,坐标值是未定义的。输入。getaccelx(),输入。getaccely(),和输入。getaccelz()返回各自加速度计的坐标值;后两种方法基于事件处理机制。它们返回KeyEvent和TouchEvent实例来记录最后一个事件触发的信息,最后一个事件在列表的末尾。

通过这些简单的接口和类,我们构建了我们的输入接口。下一节继续分析文件处理(文件输入/输出)的内容。

3.文件读写(文件输入/输出)

读写文件是游戏开发中非常重要的功能。在Java开发中,我们主要关注InputStream、OutputStream及其实例,它们是Java中读写文件的标准方法。在游戏开发中,读取资源文件,如配置文件、图片、音频文件等。,是比较常见的。保存用户进度和配置信息时,通常使用写入文件。

以下接口用于读取和写入文件:

FileIO.java

packagecom.badlogic.androidgames.framework;

导入Java.io.ioexception;

导入Java.io.InputStream;

导入Java.io.OutputStream;

公共接口FileIO{

公共输入流读取资产(字符串文件名)引发IOException

公共输入流读取文件(字符串文件名)引发IOException

公共输出流写文件(字符串文件名)引发IOException

}

在上面的代码中,我们传递一个文件名作为参数,并返回一个流,这将体现在接口的实现中。同时,我们将抛出一个IOException异常,以防止读写文件时出错。当我们读完和写完的时候,我们需要关闭iostream。Asset文件是从应用程序的APK文件中读取的,其他文件一般是从内置内存或sd卡中读取的,分别对应上述代码中的三种方法。

4.音频模块(音频)

音频模块编程一直是一个复杂的话题。这里不打算用一些高级复杂的音频处理方法,主要是播放一些背景音乐。在编写代码之前,让我们了解一下音频的基础知识。

采样率:定义每秒从连续信号中提取并由离散信号组成的样本数。采样率越高,音质越好。单位为赫兹(Hz),CD一般为44.1KHz对于每个采样系统,都会分配一定的存储位(位数)来表示声波的声学振幅状态,这称为采样分辨率或采样精度。每增加1位,表示声幅的状态数就会翻倍,6db的动态范围状态也会增加。一个2位数字音频系统可以表达成千上万种状态,也就是12db的动态范围等等。例如,16位可以表示65,536种状态,24位可以表示多达16,777,216种状态。动态范围是指声音从最弱到最强的范围,人耳的听觉范围通常为20HZ~20KHZ。高采样率意味着更多的存储空间。比如60s音,采样率8KHz,8位,约0.5M,采样率44KHz,16位,5M以上,普通3分钟流行歌曲,会超过15M。

也就是说,为了不降低质量和占用更少的空间,已经提出了许多更好的压缩方法。例如,MP3和OGGs格式是网络中流行的压缩格式。

你可以看到3分钟的歌曲占用了很多空间。当我们播放游戏的背景音乐时,我们可以流式传输音频,而不是将其预加载到内存中。通常只有一个背景音乐,所以只需要加载到磁盘一次。

对于一些短音效,例如爆炸和枪响,情况就不同了。这些短音效经常同时被调用很多次。为每个实例从磁盘流式传输这些声音效果不是一个好方法。好在短音效不会占用太多的内存空间,所以只要提前把这些音效读入内存,然后同时直接播放就可以了。

因此,我们的代码需要提供以下功能:

我们需要一种加载音频文件的方法,用于流媒体播放(音乐)和内存播放(声音),并提供控制播放的功能。

对应的接口有三个,音频、音乐、声音,代码如下。

音频接口Audio.java

packagecom.badlogic.androidgames.framework;

公共接口音频{

公共音乐新音乐(字符串文件名);

公共声音新闻声音(字符串文件名);

}

音频接口创建音乐和声音的新实例。音乐实例代表流式音频文件,声音实例代表存储在内存中的短音效。方法Audio.newMusic()和Audio.newSound()都将文件名作为参数,并引发IOException以防止文件加载失败(例如,文件不存在或文件已损坏)。

音乐界面Music.jva

packagecom.badlogic.androidgames.framework;

公共界面音乐{

公共voidplay();

publicvoidstop();

publicvoidpause();

publicvoidsetLooping(布尔循环);

publicvoidsetVolume(floatvolume);

publicbooleanisPlaying();

publicbooleanisStopped

publicbooleanIsLooping();

publicvoiddispose();

}

音乐界面有点复杂,包括播放音乐流、暂停和停止、循环以及音量控制(从0到1的浮点数)的方法。当然,有一些getter方法可以获取当前音乐实例的状态。当我们不再需要Music实例时,我们可以将其销毁(dispose方法),这将关闭系统资源,即流式音频文件。

声音接口Sound.java

packagecom.badlogic.androidgames.framework;

公共接口声音{

公共空隙播放(浮动量);

publicvoiddispose();

}

声音接口相对简单,只包括play()和dispose()方法。前者以指定的音量作为输入参数,我们可以在任何需要的时候播放音效。在后一种情况下,当我们不允许Sound实例时,我们需要销毁它来释放它占用的内存空间。

5.图像模块(图形)

最后一个模块是图像操作模块,用于在屏幕上绘制图像。但是要想画出高性能的图像,就要了解一些图像编程的基础知识。让我们从画2D图像开始。我们需要知道的第一个问题是:图像是如何绘制到屏幕上的?答案相当复杂。我们不需要知道所有的细节。

光栅、像素和帧缓冲器

现在的显示器都是基于光栅的,光栅是二维网格,也就是像素网格。光栅的长度和宽度一般用像素来表示。如果我们仔细观察显示器(或者用放大镜),可以发现显示器上有网格,是像素网格或者光栅网格。每个像素的位置可以用坐标表示,所以引入了二维坐标系,这也意味着坐标值是整数。监视器持续接收来自图形处理器的图像流,解码每个像素的颜色(程序或操作系统设置),然后将其绘制在屏幕上。每秒钟显示器会刷新多次,刷新频率单位为Hz。比如液晶显示器的主流刷新率是85Hz。

图形处理器需要从特殊的存储区域获取像素信息,以便在显示器上显示。这个区域称为视频存储区或VRAM。这个区域通常称为帧缓冲区。因此,完整的屏幕图形称为框架。对于显示网格中的每个像素,帧缓冲区中都有一个对应的内存地址。当我们需要更改屏幕上显示的内容时,我们只需要更改帧缓冲区中的内容。

下图是显示网格和帧缓冲区的简单示意图:

垂直同步和双缓冲

普通的画图方法,当要画的对象过于复杂,尤其是包含位图的时候,此时画面会显示缓慢。对于运动画面,会给人“卡死”的感觉,有时还会导致画面闪烁。所以我们使用双缓冲技术(使用两个帧缓冲器)。双缓冲原理可以形象地理解为:把电脑屏幕看成黑板。首先,我们在内存环境中构建一个“虚拟”黑板,然后在这个黑板上绘制复杂的图形。当所有图形完成后,我们将内存中绘制的图形一次性“复制”到另一块黑板(屏幕)上。这种方法可以提高绘图速度,大大提高绘图效果。以下是示意图:

要知道什么是垂直同步,首先要了解显示器的工作原理。显示器上的所有图像都是逐行扫描的。无论是隔行扫描还是逐行扫描,显示器都有两个同步参数——水平同步和垂直同步。水平同步信号决定了阴极射线管在屏幕上画一条线的时间,垂直同步信号决定了阴极射线管从屏幕顶部画到底部然后回到原来位置的时间。恰恰是垂直同步代表了CRT显示器的刷新率水平!

关闭垂直同步:我们平时运行的操作系统的屏幕刷新率一般在85Hz左右。此时显卡会每隔85Hz频率时间发送一次垂直同步信号,信号之间的时间间隔为一张85分辨率的屏幕图像写入的时间。

开启垂直同步:在游戏中,或许一个功能强大的显卡可以快速绘制一个屏幕的图像,但是没有垂直同步信号的到来,显卡无法绘制下一个屏幕,只能在85个单位的信号到来时才能绘制。这样,fps自然受到操作系统刷新率的限制。也就是说,当然,如果你的游戏屏幕的FPS数在打开后可以达到或者超过你的显示器的刷新率,那么你的游戏屏幕的FPS数就被限制在你的显示器的刷新率之内。否则,会出现不同程度的跳帧。FPS和刷新率差距越大,跳帧越严重。一般建议打卡购买高性能显卡,游戏画面会更好!打开后可以防止游戏画面高速移动时画面撕裂,比如足球直播。

关闭垂直同步,那么在游戏中完成一个画面后,显卡和显示器就可以开始绘制下一个画面图像,不用等待垂直同步信号,自然就可以充分发挥显卡的实力。

但是,不要忘了,正是垂直同步的存在,才能让游戏进程与显示器的刷新率同步,让画面流畅稳定。取消垂直同步信号,虽然可以获得更快的速度,但在图像的连续性上,性能势必会受到损害。这也是关闭垂直同步后发现画面不连续的理论原因!

图片格式

两种流行的图形格式是JPEG和PNG。JPEG是有损压缩格式,PNG是无损压缩格式,因此PNG格式可以100%再现原始图像。有损压缩格式通常占用较少的磁盘空间。我们采用什么压缩格式取决于我们的磁盘空间。与音频类似,当我们将其加载到内存中时,我们需要完全解压缩图像。因此,即使你的压缩图像在磁盘上只有20K,你在RAM中仍然需要宽度×高度×色深的存储空间。

图像叠加

假设有一个我们可以渲染的帧缓冲区,同时有几张图片加载到RAM中。我们笑称需要把RAM中的图片一张一张地放入帧缓冲区,比如一张背景图片和一张前景图片,如图所示:

这个过程叫做图像合成和叠加,我们需要将不同的图片合成为最终显示的图片。这一项画图很重要,因为上面的图总是覆盖下面的图。

上面的图像构图有一个问题:第二张图片的白色背景覆盖了第一张背景图片。我们如何擦除第二张图片的白色背景?这需要阿尔法混合。Alpha混合是通过对源点的颜色值和目标点的颜色值按照一定的算法进行运算而得到的一种透明效果。

以下是最终合成图像的RGB值,公式如下

red=src.red*src.alpha+dst.red*(1–src.alpha)

蓝色=src.绿色*src.alpha+dst.绿色*(1–src.alpha)

绿色=src.blue*src.alpha+dst.blue*(1–src.alpha)

Src和dst分别是我们需要混合的源图像和目标图像(源图像相当于人,目标图像相当于背景)。以下是一个例子。

src=(1,0.5,0.5),srcα=0.5,dst=(0,1,0)

红色=1*0.5+0*(1–0.5)=0.5

蓝色=0.5*0.5+1*(1–0.5)=0.75

红色=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,可以整体绘制,也可以部分绘制,还可以混合alpha。

获取framebuffer的长度和宽度。,android简单小游戏开发入门,这两天,没事想学习游戏开发,看了一些资料之后准备开始。为了将来编码方便,先写了一个简单的游戏框架方便自己以后做练习用。

如果以后没有什么特殊的需求–比如opengl什么的,会尽量用这个简单框架来实现。有优化的地方会在这个里边一直更新,也希望有问题的地方希望大家帮忙提一些意见

我的刷新线程基础类

/**

*我的刷新线程

*/

abstractclassLoopThreadextendsThread{

privatebooleanDEBUG=true;

privateObjectlock=newObject();

publicstaticfinalintRUNNING=1;

publicstaticfinalintPAUSE=2;

publicstaticfinalintSTOP=0;

publicintrunState=STOP;

privateWeakReference<CustomSurfaceViewCallBack>callbackRef;

publicLoopThread(CustomSurfaceViewCallBackview){

super();

callbackRef=newWeakReference<CustomSurfaceViewCallBack>(view);

}

@Override

publicvoidrun(){

try{

loop();

}catch(InterruptedExceptione){

this.interrupt();

}finally{

this.runState=STOP;

tRelase();

if(DEBUG){

System.out.println(this.getName()+”exit,lock=”+lock);

}

}

}

privatevoidloop()throwsInterruptedException{

while(runState!=STOP){

synchronized(lock){

while(runState==RUNNING){

CustomSurfaceViewCallBackcallBack=callbackRef.get();

if(callBack==null){

runState=STOP;

break;

}

onLoop(callBack);

if(runState==RUNNING){

lock.wait(500);

}else{

break;

}

}

if(runState==PAUSE){

lock.wait();

}elseif(runState==STOP){

lock.notifyAll();

return;

}

}

}

}

publicabstractvoidonLoop(CustomSurfaceViewCallBackcallBack);

publicvoidtRelase(){

callbackRef=null;

}

publicvoidtResume(){

synchronized(lock){

this.runState=LoopThread.RUNNING;

lock.notifyAll();

}

}

publicvoidtPause(){

synchronized(lock){

this.runState=LoopThread.PAUSE;

lock.notifyAll();

}

}

publicbooleantStop(){

synchronized(lock){

this.tRelase();

this.runState=LoopThread.STOP;

lock.notifyAll();

}

while(true){

try{

this.join();

returntrue;

}catch(InterruptedExceptione){

this.interrupt();

}

}

}

}

刷新接口

publicinterfaceCustomSurfaceViewCallBack{

publicabstractbooleanprocessing();

publicabstractvoiddoDraw(Canvascanvas);

publicSurfaceHoldergetSurfaceHolder();

}

游戏的显示界面的基础类

ViewCode

显示的实现还是上篇中要显示的贝塞尔曲线。当然,实现起来比原来代码简单多了

复制代码

publicclassPathActivityextendsActivity{

privateCustomSurfaceViewpathView;

@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

pathView=newPathView(this);

pathView.setSingleThread(false);

this.setContentView(pathView);

}

privatestaticclassPathViewextendsCustomSurfaceView{

privateintvWidth;

privateintvHeight;

privatePaintpPaint;

privatePathmPath;

privatefloatendX;

privatefloatendY;

privateRandomrandom;

publicPathView(Contextcontext){

super(context);

pPaint=newPaint();

pPaint.setAntiAlias(true);

pPaint.setColor(0xaf22aa22);

pPaint.setStyle(Paint.Style.STROKE);

mPath=newPath();

random=newRandom();

}

privatevoidinit(){

vWidth=getWidth();

vHeight=getHeight();

endX=0.8f*vWidth;

endY=0.8f*vHeight;

}

@Override

publicvoidsurfaceCreated(SurfaceHolderholder){

super.surfaceCreated(holder);

init();

}

@Override

publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,

intheight){

super.surfaceChanged(holder,format,width,height);

init();

}

@Override

publicvoiddoDraw(Canvascanvas){

canvas.drawColor(Color.BLACK);

canvas.drawPath(mPath,pPaint);

}

@Override

publicbooleanprocessing(){

//这里构建贝塞尔曲线

mPath.reset();

mPath.moveTo(50,50);

mPath.quadTo(random.nextInt(vWidth),random.nextInt(vHeight),endX/2,random.nextInt(vHeight));

mPath.quadTo(random.nextInt(vWidth),random.nextInt(vHeight),endX,endY);

returntrue;

}

}

}

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容

图片-小白之家
图片-小白之家