本文内容整理自Android 4游戏入门经典
一、基础游戏框架
任何游戏都需要一些基本框架,用于实现抽象化,并减轻与底层操作系统交互的痛苦。通常这一框架分成几个模块:
应用程序和窗口管理
用于创建一个窗口和处理一些操作,如关闭窗口、暂停或恢复Android应用程序等。
输入
与窗口管理模块相关联,用于跟踪用户的输入,如触摸事件、按键事件、加速计读取等。
Android系统中,这些输入会被调度到当前具有焦点的窗口中,你可以很容易的注册和记录这些事件。
而这些输入的消费方式通常有两种:
- 轮询,消费方通过轮询检查输入设备的当前状态,这种方式适用于检查用户输入,但是注意,这些事件的顺序将会丢失;
- 基于事件的处理,通过事件队列保存事件,然后一个一个处理,从而保证事件的时间顺序。
下面是一个用于轮询触摸屏、键盘、加速计的接口,同时它可以访问触摸屏和按键事件:
public interface Input {
public static class KeyEvent {
//表示按下按键
public static final int KEY_DOWN = 0;
//表示放开按键
public static final int KEY_UP = 1;
//按键类型:按下或放开
public int type;
//按键的键码
public int keyCode;
//按键的Unicode字符。按键按下和放开的Unicode字符可能是不同的,因为可能同时按下了shift键或其他类似作用的键。所以此处需要保存Unicode信息
public char keyChar;
}
public static class TouchEvent {
//按下
public static final int TOUCH_DOWN = 0;
//抬起
public static final int TOUCH_UP = 1;
//拖动
public static final int TOUCH_DRAGGED = 2;
public int type;
public int x, y;
//保存此触摸事件的手指的指针索引,用于多点触摸情景的处理
public int pointer;
}
public boolean isKeyPressed(int keyCode);
public boolean isTouchDown(int pointer);
public int getTouchX(int pointer);
public int getTouchY(int pointer);
//加速计相关接口。注意,加速计只能使用轮询的方式,而不能通过事件的方式
public float getAccelX(int pointer);
public float getAccelY(int pointer);
public float getAccelZ(int pointer);
public List<KeyEvent> getKeyEvents();
public List<TouchEvent> getTouchEvents();
}
文件I/O
允许从硬盘上将资源文件读取到应用程序中。
下面是简单接口的定义:
public interface FileIO{
public InputStream readAsset(String fileName) throws IOException;
public InputStream readFile(String fileName) throws IOException;
public OutputStream writeFile(String fileName) throws IOException;
}
音频
该模块负责加载和播放一些我们能听到的声音。
音频从使用方面可以分为音乐和音效。
- 音乐。指比较大的音频,如果预加载到内存中会占用大量内存,所以需要以流式方式播放,如背景音乐。
- 音效。指较短的音频,如爆炸声、枪击声等,通常需要多次、同时的播放这类音频,所以一般会直接将其读取到内存中,然后直接从内存中播放。
音频的简单接口定义如下:
public interface Audio {
public Music newMusic(String fileName);
public Sound newSound(String fileName);
}
public interface Music {
public void play();
public void stop();
public void pause();
public void setLooping(boolean looping);
public void setVolume(float volume);
public boolean isPlaying();
public boolean isStopped();
public boolean isLooping();
public void dispose();
}
public interface Sound {
public void play(float volume);
public void stop();
public boolean isPlaying();
public boolean isStopped();
public void dispose();
}
图形
这是游戏开发中除实际游戏外最复杂的模块,它负责加载图形并绘制在画面上。
首先我们要明确几个概念:
- 光栅。
现在的显示器是基于光栅的,它是一种图像元素的2D网格,也可以理解为像素。光栅网格具有宽度和高度,通常用每行/每列的像素总数表示。 - 像素。
像素是光栅上面的一个点,其具有两个属性:位于光栅的位置和颜色。 - 像素的位置。
像素的位置用一个离散坐标系统中的2D坐标表示,此2D坐标系的原点位于光栅的左上角,正X轴向右,正Y轴向下。注意,X轴坐标的最大值为光栅宽度减1,Y轴坐标的最大值为光栅高度减1。 - 像素的颜色。
- 刷新率。
显示器接收来自图形处理器的信息流后会刷新自身的状态,这里的刷新速度称为刷新率,单位为赫兹。 - 帧缓冲区。
图形处理器会从一个特殊的内存空间读取信息并显示在显示器上,这个特殊的内存空间称为视频随机访问内存或者VRAM,也叫帧缓冲区。显示器光栅的每个像素在帧缓冲区都有一个对应的内存地址,用于记录该像素的颜色。当我们想改变显示器的显示时,只需要改变帧缓冲区中的像素的颜色即可。 - 双缓冲区。我们不知道显示器什么时候会从缓冲区中读取数据,为了防止在写入新数据时显示器读取数据,我们采用双缓冲区的方式:维护一个前端缓冲区和一个后端缓冲区。前端缓冲区用于显示,后端缓冲区用于绘制下一帧。
- 垂直同步。双缓冲区不能完全解决上面提到的问题,因为显示器在刷新其内容时,仍有可能向缓冲区写数据,所有引入垂直同步,当对缓冲区进行读写时,GPU将被阻塞,直到显示器发出信号说已完成刷新。
- 颜色的表示。
通常我们使用的颜色模型为RGB模型,其用红绿蓝三色的混合表示所有的颜色。RGB模型有三个分量,每个分量的值介于0.0-1.0之间,表示每个分量的颜色的占比。但是如果用浮点数表示RGB模型,将会需要12个字节/24个字节的空间(取决于使用的浮点数是32位还是64位)。为了节省空间,我们用一个无符号字节表示一个RGB分量,每个分量的强度值范围是0~255,这样表示一个像素就只需要3个字节了。
当然,我们还可以用其他方式表示RGB模型,如用一个字(16位)表示一个像素,红色分量用5位表示,绿色用6位,蓝色用5位,这里我们不做研究。 - alpha合成。
alpha合成的知识相对复杂,本文不做描述。
在知道上面的概念后,我们可以给出一个简单的图像接口:
public interface Graphics {
//像素的编码格式,如ARGB8888表示数据按照alpha、red、green、blue顺序存放分量数据,每个分量占8位
public static enum PixmapFormat {
ARGB8888, ARGB4444, RGB565
}
//使用JPEG或PNG加载一幅图片
public Pixmap newPixmap(String fileName, PixmapFormat format);
//用指定颜色清除整个帧缓冲区原来的颜色
public void clear(int color);
public void drawPixel(int x, int y, int color);
public void drawLine(int x, int y, int x2, int y2, int color);
public void drawRect(int x, int y, int width, int height, int color);
public void drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY, int srcWidth, int srcHeight);
public void drawPixmap(Pixmap pixmap, int x, int y);
public int getWidth();
public int getHeight();
}
public interface Pixmap {
public int getWidth();
public int getHeight();
public Graphics.PixmapFormat getFormat();
public void dispose();
}
游戏框架
游戏框架集合了上面的所有部分,为我们编写游戏提供了一个易用的基础。
在讨论真正的游戏框架前,我们需要明确游戏要完成的功能:
- 游戏有不同的画面,但是这些画面做的工作都类似:评估用户输入、将输入转变为画面状态、渲染场景等。有的画面可能不需要用户输入,但是会在一段时间后切换到另一界面。
- 画面需要用某种方式进行管理。
- 游戏画面需要响应不同的模块,如图形、声音、用户输入,这样才能加载资源、捕获用户输入、播放声音、渲染帧缓冲区等。
- 游戏一般是实时的,我们必须尽可能多的更新当前画面的状态并进行渲染。这个过程一般在一个主循环中进行,只有在游戏退出后该循环才会终止。循环的一次简单迭代成为一帧,每秒我们进行的帧数称为帧率。
- 我们需要追踪上一帧到现在的时间间隔,它用于帧无关的运动中。
- 游戏需要保持跟踪窗口状态,如暂停、恢复,并用事件通知当前画面。
- 游戏框架将负责设置窗口和UI组件。
下面我们设计一个简单的游戏接口,它将:
- 创建窗口和UI组件并连接到回调函数。
- 启动主循环线程。
- 跟踪当前画面,并在每一次主循环迭代中都更新并显示它。
- 将所有窗口事件从UI线程传输给主循环线程,并将它们递交给当前画面以改变画面状态。
- 开放我们前面设计的那些模块: Input、FileIO、Graphics、Audio。
下面是一个非常简单的Game接口,它隐藏了所有复杂性,另外是一个抽象的Screen类,表示画面:
public interface Game {
public Input getInput();
public FileIO getFileIO();
public Graphics getGraphics();
public Audio getAudio();
public void setScreen(Screen screen);
public Screen getCurrentScreen();
public Screen getStartScreen();
}
public abstract class Screen {
protected final Game game;
public Screen(Game game) {
this.game = game;
}
public abstract void update(float deltaTime);
public abstract void present(float deltaTime);
public abstract void pause();
public abstract void resume();
public abstract void dispose();
}
实际上游戏框架还应该包含网络编程,不过这部分比较高级,如果对该主题感兴趣,可以到www.gamedev.net找相关教程