游戏框架的架构
接口(Interface)的出现可以说是人类智慧的结晶,20世纪最伟大的研究成果之一. 软件危机的产物.
对于大多数的面向对象初学者来说,接口是一个非常神秘的东西,常常让人摸不着头脑,”到底这东西有什么用,何必绕圈子呢?”,
这是大家一致通过的”名言”,正是这”名言”,左右着我们深入去探索接口的奥妙.
随着对知识的不断扩展,经验的不断增加,我们开始发现评价一个系统的好坏,不在于系统的功能如何完善,如何新颖,而是在于:
”保证系统完成基本功能的情况下这个系统是否可以再生,是否易于维护,通用性如何”,因此,”如何结构性地,有条理地开发系统和
编写代码才能使整个系统易于扩展,易于维护”成了我们每一个程序设计人员最为关注的话题之一.
于是,人们注意到了接口…
其实我们可以把它和现实生活联系起来会比较容易理解.
我们都知道,现在数码市场上的产品琳琅满目,各种各样,花样百出可谓是百家争鸣,不过细心的你应该都知道,这些数码产品都有一个
共同的特点:可以通过一种叫做”数据线”的东西和计算机相互连接,交换信息.
看到这里,是否会恍然大悟?对呀,为什么各种不同的数码产品,不同的设备都可以和电脑连接呢?呵呵,道理很简单,如果不明白其中的
原理我来告诉你吧.
这些东西都是通过同一种接口,USB接口(通用串行总线接口)和计算机连接的,我们计算机方面暂且不去讨论,我们只讨论数码产品那边
的吧.无论是数码相机,MP3,手机,掌上游戏机都好,如果要和电脑连接都要在这些设备里边实现一系列的协议(我们把它叫做”约定”或
者契约”吧),这些协议有很多,其中包括有:并行控制,串行控制,USB等,而且这些协议在电脑上都支持的,假如我们在某个设备里边实现
了USB协议,那么根据那个协议的规定,我们就可以把电脑通过USB数据线发送给该设备的数据,在数据准备发送前,通过该设备的驱动程序,
将数据转化成可以识别的信息,然后发送,同样道理,该设备要发送数据给计算机的时候,在设备里先会把要发送的信息转化成USB协议合
法的信息格式(该格式是可以被计算机识别的),然后通过数据线发送给计算机,计算机将他还原(又或者在计算内通过驱动程序来转换).但
从头到晚,计算机就好像完全不用考虑到底对方是何方神圣,是什么东西,它只考虑对方可以发出我认识的信号,而它看到这个信号二话不
说,没有厌言就会自动阅读那信号,根据信号内容做出不同的反应.
其道理又相当于模拟有线电视那样,电缆传来卫星电视模拟信号,然后在电视机里通过一种约定好的协议把这总模拟信号再生为电视机可
以识别的,然后有经过一系列的处理,在显示屏上显示出来.而各种不同的厂家生产的电视机是可以通用同一种电缆来收看有线电视的,其
中也是接口在起这种通用性的控制.
好了,我们谈的是面向对象程序设计里的接口,如果你还不懂我上边的意思的话,请看下边的两段代码(我这里假设你是一个无线J2ME应用
开发者或者有面向对象的相关编程经验可以阅读Java代码的人),其实那两段代码的功能是完全相同的,只是表达方式不同,实现的方式不
同而已.第一种方法是传统的做法,就是结构化程序设计的那种做法,第二种是采用面向对象的设计方法,这里利用了接口,让我们来看看
其中的奥妙吧
方法一
import javax.microedition.lcdui.*;
public class GameForm extends Canvas implements Runnable{
public static final int DELAY_TIME=30; //游戏刷新率控制
public static final int STAGE_LOGO=0; //开发商LOGO
public static final int STAGE_TITLE=1; //游戏主菜单,游戏标题画面
public static final int STAGE_GAMEING=2; //游戏运行中
public static final int STAGE_HELP=3; //帮助
public static final int STAGE_OPTION=4; //选项设置
public static final int KEY_NONE=0; //无按键
public static final int KEY_UP=1; //上
public static final int KEY_DOWN=2; //下
public static final int KEY_LEFT=3; //左
public static final int KEY_RIGHT=4; //右
int gameStage; //游戏状态控制
int keyState; //游戏按键状态(上下左右)
//......
int keyCode; //按键编码
public GameForm(){
//......
gameStage = STAGE_LOGO;
new Thread(this).start();
}
//特别提示:该处不要多做其它动作,因为keyPressed()是在另外一个线程里运行的,
//所以尽量不要把代码放在这里处理,我们只要获得按键编码就可以退出了
public void keyPressed(int code){
keyCode = code; //获得输入按键遍码
}
//游戏主线程,主循环
public void run(){
while(true){
getKeyState(); //获得按键状态
dealKeyState(); //处理刚刚获得的按键状态,即:响应该按键
dealGameStage(); //响应按键完毕,更新游戏中的各种数据
repaint(); //把最新的结果显示出来
try{ //刷新率控制
Thread.sleep(DELAY_TIME);
}catch(Exception e){
e.printStackTrace();
}
}
}
//这里把获得的按键编码转化成游戏可以识别的按键状态
public void getKeyState(){
int gameAction=getGameAction(keyCode);
if(keyCode==KEY_NUM2||gameAction==UP){
keyState=KEY_UP;
}else if(keyCode==KEY_NUM8||gameAction==DOWN){
keyState=KEY_DOWN;
}else if(keyCode==KEY_NUM4||gameAction==LEFT){
keyState=KEY_LEFT;
}else if(keyCode==KEY_NUM6||gameAction==RIGHT){
keyState=KEY_RIGHT;
}
}
public void dealKeyState(){
//根据不同的游戏状态对对获得的按键进行响应
switch(gameStage){
case STAGE_LOGO:
break;
case STAGE_TITLE:
break;
case STAGE_GAMEING:
//这些部分省略了非常多的代码
break;
case STAGE_HELP:
break;
case STAGE_OPTION:
break;
}
}
public void dealGameStage(){
//根据不同的游戏状态,更新相应的数据,从而达到处理各种不同状态的效果
switch(gameStage){
case STAGE_LOGO:
break;
case STAGE_TITLE:
break;
case STAGE_GAMEING:
//这些部分省略了非常多的代码
break;
case STAGE_HELP:
break;
case STAGE_OPTION:
break;
}
}
public void paint(Graphics g){
//根据不同的游戏状态,绘制不同的界面
switch(gameStage){
case STAGE_LOGO:
break;
case STAGE_TITLE:
break;
case STAGE_GAMEING:
//这些部分省略了非常多的代码
break;
case STAGE_HELP:
break;
case STAGE_OPTION:
break;
}
}
}
以上是第一种游戏的基本框架编码风格,可以说被目前基本50%以上的国内的手机游戏开发者广泛的使用,原因很简单,框架如果不包涵其它
任何关于游戏内容有关的东西的话,一目了然,很容易上手,也很容易被一般的开发者想到
方法二
import javax.microedition.lcdui.Graphics;
//游戏状态和游戏主逻辑相互通信的接口
//该接口定义了三个方法,如下
public interface GameState{
public void dealKeyState(int keyState); //对游戏主逻辑传来的按键状态进行处理
public void dealGameState(); //按键响应完毕,更新游戏数据
public void paint(Graphics g); //把最新的画面显示出来
}
import javax.microedition.lcdui.*;
public class GameForm extends Canvas implements Runnable{
public static final long DELAY_TIME=50 //游戏刷新率控制
public static final int STAGE_LOGO=0; //开发商LOGO
public static final int STAGE_TITLE=1; //游戏主菜单,游戏标题画面
public static final int STAGE_GAMEING=2; //游戏运行中
public static final int STAGE_HELP=3; //帮助
public static final int STAGE_OPTION=4; //选项设置
//…….省略了其它状态
public static final int KEY_NONE=0; //无按键
public static final int KEY_UP=1; //上
public static final int KEY_DOWN=2; //下
public static final int KEY_LEFT=3; //左
public static final int KEY_RIGHT=4; //右
int gameStage; //游戏状态控制
int keyState; //游戏按键状态(上下左右)
//......
int keyCode; //按键编码
GameState state; //一个GameState接口,用来和游戏状态通信
public void GameForm(){
//......
changGameStage(STAGE_LOGO); //新方法,改变游戏状态
new Thread(this).start();
}
public void keyPressed(int code){
keyCode = code; //获得输入按键遍码
}
/**游戏主线程,主循环**/
public void run(){
long timeStart;
long dTime;
while(true){
timeStart=System.currentTimeMillis();
/**获得游戏安装响应,并映射成程序可以理解的按键状态**/
getKeyState();
/**处理刚刚获得的新按键状态**/
dealKeyState();
/**更新游戏状态**/
dealGameState();
/**更新屏幕**/
repaint();
/**刷新率控制**/
dTime=System.currentTimeMillis() - timeStart;
if(dTime < DELAY_TIME){
try{
Thread.sleep(DELAY_TIME - dTime);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
//这里把获得的按键编码转化成游戏可以识别的按键状态
public void getKeyState(){
int gameAction=getGameAction(keyCode);
//给出部分按键,各种机型不完全兼容
if(keyCode==KEY_NUM2||gameAction==UP){
keyState=KEY_UP;
}else if(keyCode==KEY_NUM8||gameAction==DOWN){
keyState=KEY_DOWN;
}else if(keyCode==KEY_NUM4||gameAction==LEFT){
keyState=KEY_LEFT;
}else if(keyCode==KEY_NUM6||gameAction==RIGHT){
keyState=KEY_RIGHT;
}
keyCode = KEY_NONE; //要处理连续按键可以处理这个地方
}
//以下部分注意和方法一的不同点
public void dealKeyState(){
state.dealKeyState(keyState);
}
public void dealGameState(){
state.dealGameState();
}
public void paint(Graphics g){
state.paint(g);
}
//改变游戏的状态
public void changeGameState(int s){
state = null; //清空上一次的游戏状态
System.gc(); //释放内存,其实可以不用,KVM过一段时间会自动示范释放无用的对象占用的空间的
//根据不同的状态,改变接口的"处理类型",使新的接口可以准确的和游戏逻辑通信
switch(s){
case STAGE_LOGO:
state = Logo(this);
break;
case STAGE_TITLE:
state = Title(this);
break;
case STAGE_GAMEING:
state = Gameing(this);
break;
case STAGE_HELP:
state = Help(this);
break;
case STAGE_OPTION:
state = Option(this);
break;
}
}
}
//Logo.java
public class Logo implements GameState{
GameForm main;
public Logo(GameForm m){
main = m;
}
public void dealKeyState(int keyState){
//这里的代码为处理Logo专用
}
public void dealGameState(){
//这里的代码为处理Logo专用
}
public void paint(Graphics g){
//绘制LOGO画面
}
}
//Title.java
public class Title implements GameState{
GameForm main;
public Title(GameForm m){
main = m;
}
public void dealKeyState(int keyState){
//这里的代码为处理Title专用
}
public void dealGameState(){
//这里的代码为处理Title专用
}
public void paint(Graphics g){
//绘制Title画面
}
}
//Gameing.java
public class Gameing implements GameState{
GameForm main;
public Gameing(GameForm m){
main = m;
}
public void dealKeyState(int keyState){
//这里的代码为处理Gameing专用
}
public void dealGameState(){
//这里的代码为处理Gameing专用
}
public void paint(Graphics g){
//绘制Gameing画面
}
}
//Help.jave
public class Help implements GameState{
GameForm main;
public Help(GameForm m){
main = m;
}
public void dealKeyState(int keyState){
//这里的代码为处理Help专用
}
public void dealGameState(){
//这里的代码为处理Help专用
}
public void paint(Graphics g){
//绘制Help画面
}
}
//Option.java
public class Option implements GameState{
GameForm main;
public Option(GameForm m){
main = m;
}
public void dealKeyState(int keyState){
//这里的代码为处理Option专用
}
public void dealGameState(){
//这里的代码为处理Option专用
}
public void paint(Graphics g){
//绘制Option画面
}
}
以上是第二种建立游戏框架的方法,稍有经验的人一眼就可以看出它的好处:
1.) 主程序部分的代码简洁明了;
2.) 代码可扩展性非常强;
3.) 可以减少过多的内容放在同一个对象里边被同时装入运行时堆栈,减少运行时的内存开销(这里相对于大的程序来说的,
如果程序比较少,和方法一相差不多)
4.) 可以非常方便团队合作开发;(这点是相当重要)
这里把两个方法比较一下重点分析一下方法二吧:
1.) 假如你的游戏有上万行,两三万行以上,你会发现方法一维护起来非常麻烦,有时会花好几秒,甚至更长的时间寻找你需要修改编辑
的代码;而采用方法二,要修改编辑的部分寻找起来非常快捷方便,心情自然也会愉快起来.
2.) 假如你手头上有几个项目要赶(有些老板叫你一个月赶出一个来),如果你采用方法二,你会发现你写代码那么多,竟然没有什么东西
以前可以用上的(这里指代码),最多是把以前的代码翻出来,建立新工程,删除掉没有用的部分,而采用方法二,你会发现那个类
GameForm还有接口GameState(这里省了很多有用的类和接口没有列出,比如:处理游戏存档,游戏声音,游戏排行榜这些东西,我们
完全可以利用接口实现的)可以直接拿过来用,完全不用修改(除非你在找BUG).
3.) 如果你这项目是一个大型的RPG,甚至是MMORPG你怎么办呢?采用方法一?呵呵,我可以实话告诉你,那可能会相当麻烦,你可能(你们
可能),甚至要花上整个游戏开发周期的相当多的时间(具体多少也说不准)来相互沟通,协调代码的一致性,因为你们可能同一时间
编辑到相同的类甚至是同一函数,而你的合作者一个在广州而另外一个可能在香港(尽管CVS可以减少我们不少麻烦,不过还是不能
避免采用方法一带来的非常多的麻烦).而采用方法二,是标准的面向对象设计模式开发方法,我们在整个项目的概要设计阶段已经
通过整个团对的开会讨论,设计好了各种不同的接口以及接口的方法(这是整个团队的约定,契约,任何人都不可以违约,否则影响
自己,影响他人),然后画出整个模型结构图,编写开发文档,然后由项目主管分配好了每个人的任务(每个人要实现哪些类,哪些方
法等),就可以开始各自工作了,工作的依据是根据那个开发文档,整个过程是通过接口联系的(我们只知道那个接口有什么用的,我
们尽量去用他,不用考虑我的伙伴在那边是怎样去实现它的,因为”它就是它”)非常科学,这也充分体现了面向对象的封装性.
4.) 如果你是一个引擎设计者,方法二提供你参考.
通过对以上两个方法种可以看出,无论哪一个方面,方法二都站有非常大的优势.有人会问,不是说手机上运行的程序,越少对象越好吗?
没错,是这样的,可以节省空间,不过方法二没有哪个地方不节省的啊?
即使方法而看起来会非常多的类,但运行的时候,在内存是根据玩家的操作来出现的,就是说,比如:Help(帮助界面) Option(选项界面),
这两个界面是不可能同一时间在同一屏幕出现的吧?
反而,如果你用方法一,如果你的代码有非常多在主类的话,你运行的时候会占用非常多的代码段地址空间(用来保存某行代码的入口地
址的堆栈),这才叫空间浪费呀!而我们可以看到,方法二的主类占用的空间是相当少的(相比方法一).
好的框架对一个游戏有着至关重要的影响,以上介绍提供参考,还是要具体问题具体解决才是,实践是检验真理的唯一标准,希望对各位
网友们有所帮助,这也是我花宝贵的时间写开发日志的最大心愿,同时也希望大家把自己宝贵的经验贡献出来,谢谢!