Android杂的记中篇

游戏开发

手机游戏开发需要开发者用面向对象的方式定义界面上的所有角色和能与玩家交互的元素,同时需要设计好游戏的动画管理、动画后台线程支持、游戏规则的判定、角色行为控制等。

框架搭建

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

  • 窗口管理(Window management):该模块负责在Android平台上创建、运行、暂停、恢复游戏界面等功能。

    我们可以把游戏的窗口想象成一个可以在它上面绘制内容的画布。窗口管理模块负责定制窗口、添加各种UI组建、接受各类用户的输入事件。这些UI组件或许可以通过GPU等硬件加速(比如使用了OpenGL ES)。

    该模块设计的目的不是提供接口,而是和游戏框架整合在一起。我们需要记住的是应用程序状态和窗口事件是该模块必须处理的事情:

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

    Pause: 当应用程序由于默写原因暂停时调用的方法。

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

  • 输入模块(Input):该模块和视窗管理模块是密切相关的,用来监测追踪用户的输入(比如触摸事件、按键事件、加速计事件等)。

    大部分操作系统中,输入事件( 比如触屏事件、按键事件)是通过当前的窗口调度(dispatched)的,窗口再进一步把这些事件派发给当前选中的组件。因此我们只需要关注组件的事件即可。操作系统提供的UI APIs提供了事件分发机制,我们可以很容易地注册和监听事件,这也是输入模块的主要职责。有两种处理事件的做法:

    轮询(Polling):在这种机制下,我们仅检查输入设备的当前状态,之前和之后的状态有无保存。这种输入事件处理适合处理诸如触屏按钮事件,而不适合跟踪文本的输入,因为按键事件的顺序并未保存。

    基于事件的处理(Event-based handling):这种机制提供了记忆功能的事件处理,比较适合处理文本输入或者其他需要按键次序的操作。有两种方式:监听和回调。

    在Android平台中,主要有三种输入事件:触屏事件、按键事件和加速计事件,前两种时间使用轮询机制和基于事件处理的机制都适合,加速计事件通常是轮询机制。

    触屏事件有三种:

    Touch down: 手机触屏时发生。

    Touch drag: 手指拖动时发生,此前有Touch down事件产生。

    Touch up: 手指抬起时发生。

    每种触摸事件有相关的辅助信息:触屏的位置、指针索引(多点触摸时用来追踪识别不同的触点)

    键盘事件包括两种:

    Key down: 按下键盘时触发。

    Key up: 释放键盘时触发。

    每种按键事件也有相关的辅助信息:Key-down事件存储按键码,Key-up事件存储按键码和实际的Unicode字符。

    加速计事件,系统不停的轮询加速计的状态,并以三位坐标标识。

  • 文件输入输出(File I/O):此模块用来读取assets文件下图片、音频等资源。

    读写文件在游戏开发中是一项十分重要的功能。在Java开发中,我们主要关注InputStream和OutputStream及其实例,它们是Java中读写文件的标准方法。

    游戏开发中,比较多的是读取资源文件,比如配置文件、图片、音频文件等。写入文件的操作一般在保存用户进度和配置信息时使用。

  • 图像模块(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 ×color depth的存储空间。

    • 图像叠加

      假定有一个我们可以渲染的帧缓冲区(framebuffer),同时有几个加载到RAM中的图片,我们笑需要把RAM中的图片逐次放入到帧缓冲区,比如一个背景图片和一个前景图片如图所示:

      这里写图片描述

      这个过程就叫做图像的合成和叠加,我们需要把不同的图片合成一个最终显示的图片。绘制图片的此项很重要,因为上面的图片总会覆盖下面的图片。
      这里写图片描述
      上面图像合成出现了问题:第二张图片的白色背景覆盖了第一张背景图片。我们怎样把第二张图的白色背景消去呢?这就需要alpha混合(alpha blending)。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 * (10.5) = 0.5
      blue = 0.5 * 0.5 + 1 * (10.5) = 0.75
      red = 0.5 * 0.5 + 0 * (10.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即可。最终效果如下图:
      这里写图片描述

    • 图像模块的接口代码

      通过以上介绍,我们可以开始设计我们的图像模块的接口。需要实现如下功能:

      1. 从磁盘加载图片到内存中,为以后绘制到屏幕做准备。

      2. 用特定颜色清除framebuffer

      3. 用指定颜色在framebuffer指定位置绘制像素。

      4. 在framebuffer上绘制线条和矩形。

      5. 绘制上面内存中的图片到framebuffer,能够整个绘制和部分绘制,alpha混合绘制。

      6. 得到framebuffer的长宽。

      这里用两个接口来实现:Graphics和 Pixmap

  • 音频模块(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接口创建新的Music和 Sound实例。一个Music实例表示一个流音频文件,一个Sound实例表示一个保存在内存中的短的音效。方法 Audio.newMusic()和Audio.newSound()都是以文件名作为参数并抛出IOException以防文件加载失败(例如文件不存在或者文件损坏等情况)。

    Music接口有点复杂,包含了播放音乐流、暂定和停止、循环播放、音量控制(从0到1的浮点数)方法。当然,里面还有一些getter方法,用来获取当前音乐实例的状态。当我们不再需要Music 实例时, 我们可以销毁它(dispose方法),这会关闭系统资源,即流化的音频文件。

    Sound接口比较简单,只包含play()和dispose()方法。前者以指定的音量为输入参数,我们可以在任何我们需要的时候播放音效。后者在我们不许Sound实例时,我们需要销毁它以释放它占用的内存空间。

  • 网络(networking):如果游戏提供多人游戏联网功能,此模块就是必须的。

  • 游戏框架(Game framework):该模块把以上各种模块整合起来,提供一个易用的框架,来轻松地实现我们的游戏。

    所有的基础工作做完后,我们最后来探讨一下游戏框架本身。我们看下为了运行我们的游戏,还需要什么样的工作要做:

    • 游戏被分为不同的屏幕(screen),每个屏幕执行着相同的任务:判断用户输入,根据输入渲染屏幕。一些节目或许不需要任何用户输入,但会过段时间后切换到下一屏幕.(如Splash界面)

    • 屏幕需要以某种方法被管理(如我们需要跟踪当前的屏幕并且能随时切换的下一屏幕)

    • 游戏需要允许屏幕访问不同的模块(比如图像模块、音频模块、输入模块等),这样屏幕才能加载资源,获取用户输入,播放声音,渲染缓冲区等。因为我们的游戏是实时游戏,我们需要当前的屏幕快速的更新。我们因此需要一个主循环来实现。主循环在游戏退出时结束。每次循环迭代成为一帧,每秒帧的次数我们成为帧速(FPS).

    • 游戏需要追踪窗口的状态(如是否暂停游戏或者恢复等),并通知产生相应的处理事件。

    • 游戏框架需要处理窗口的建立、UI组件的创建等

    代码首先创建了游戏的窗口和UI组件,接着我们实例化了基本的组件,这些能保证游戏基本功能的实现。我们又实例化了我们的起始屏幕,并把它作为当前的屏幕。然后记下当前的时间。

    接着我们进入了主循环,当用户想退出时我们可以结束主循环。在主循环里面,计算上一帧和当前帧的时间差,用来计算FPS。最后,我们更新了当前屏幕的状态并呈现给用户。updateState方法依赖时间差和输入状态,present方法包括渲染屏幕的状态到framebuffer,播放音频等。present方法也需要知道上次调用到现在的时间差。

    当主循环结束后,我们就需要清理和释放各种资源了。

    这就是游戏工作的流程:处理用户的输入、更新状态、并呈现给用户。

    • 游戏和显示接口

      1. 建立窗口进和UI,并建立相应的事件机制

      2. 开启游戏的主循环

      3. 跟踪当前的屏幕显示,在每次主循环中让其更新

      4. 把UI线程中的事件转移到主线程中,并把这些事件传递给当前显示界面,以便同步变化。

      5. 确保能访问所有的游戏基本模块,如Input, FileIO,Graphics, 和 Audio.

数学、物理、ai知识

  • 数学

    两点距离、三角函数

  • 物理

    一维空间运动(速率)、加速度、力(重力、支持力、摩擦力【静摩擦、滑动摩擦】)、牛顿定律、动量和冲量(碰撞)

  • AI

    游戏规则分为三大部分,规则对象,规则事件和相应规则。而我们所述说的对象主要是玩家控制的角色,当然非控制角色也有着自己的规则和事件,这时就需要我们赋予它们以类似于人类的智能。所以我们可以简化为一句话:让游戏中的npc获得分析,判断的能力,并进行相应行为的设计,我们称之为游戏ai设计。

    ai对游戏起到了什么作用呢?

    1、增加玩家的挑战性
    2、创造更真实的虚拟世界
    3、增加游戏的可玩性
    4、辅助其他功能

    ai设计的类型

    1、fsm(finite state machine)有限状态机。这是最简单也是最古老的ai技术,如果懂得程序的人员,可以将其简单的理解为一套if,else或者 switch,case构成的条件判断。

    2、fusm(fuzzy state machine)模糊状态机。对于它的名词解释或许会很复杂饶口,而我们没必要去关系那些,我们可以最简单的理解其真意:就是在fsm中加入随机特性。

    3、可扩展性ai。这个概念可能会很大程度上引起的兴趣,而实际上它并非那么神秘,我们rm使用者实际上一直在接触着它。看这帖子的朋友里我相信有相当一部门并不懂得编程,而实际上,程序员们不关心游戏中的平衡问题,他们需要做的仅仅是实现游戏的功能,至于以如何使用百分比来控制游戏进程,他们一点也不管。那么我们开始罗嗦了这么久的fsm,fusm是给谁看的?呵呵,是给策划看的,是给我们自己看的。是的,这样做仅仅是方便我们的调整和逻辑判断。通常,程序员们会制作一套工具来让我们进行设置和测试,而这套工具就是可扩展性ai。

猜你喜欢

转载自blog.csdn.net/johnWcheung/article/details/80213981