游戏开发,好大的一个命题哦。无论是游戏的剧情设计,还是游戏的美工制作,那都不是一两下子能够完成的事,而编程,只是其中的一小部分。但是,就算是这一小部分,我也不可能把它掌握得很透彻。这篇随笔,大部分都是些抄书的东西,主要是为了我的博客的完整性而存在,但是我依然会尽力把它写好,要让那些精通Java其它领域但是却没有机会做J2ME开发的高手们看看图片解解馋,让那些想做手机游戏开发的同仁们看看J2ME的Game API究竟为我们提供了哪些支持,让我们知道写游戏需要了解哪些概念。
先来了解一下MIDP 2.0的游戏开发包,不用怕,这组API很简洁,只有区区5个类,它们都位于javax.microedition.lcdui.game包中。它们分别为GameCanvas类、Layer类、LayerManager类、Sprite类和TiledLayer类。下面分别来介绍一下各个类的用途。
1> GameCanvas类 GameCanvas类是Canvas的子类,它代表了游戏的基本界面,简单一点说,就是所有的游戏画面都是在GameCanvas上进行绘制的。那么GameCanvas和Canvas相比,究竟提供了些什么更高级的功能呢?主要有两点:1、实现了双缓冲功能;2、提供了轮询键盘输入事件的方法。
双缓冲大家肯定很熟悉,就是我们绘图的时候先把图象绘制到一个缓冲区中,等图象绘制完成后,再一次性显示到屏幕上,这样,就可以有效消除闪烁和画面撕裂等现象。在GameCanvas中,我们可以通过getGraphics()方法取得缓冲区的Graphics对象实例,可以通过flushGraphics()方法来将缓冲区的内容显示到屏幕上。
轮询键盘输入事件有什么用呢?要回答这个问题,我们首先要了解一些基本的游戏框架,在大部分游戏中,都存在一个主循环,这个主循环决定了我们的游戏以每秒钟多少步的频率运行,而在每一步中,先查询有无键盘输入事件,再运行游戏的逻辑运算,最后更新画面,然后再进入下一步。在Canvas中,键盘的输入事件都是通过回调的方式进行的,也就是说当有按键按下的时候,调用keyPressed()方法,这样的功能肯定不能够满足我们这样的每一步都要查询键盘输入的要求。通过调用getKeyStates()方法即可轮询键盘。
2> Layer类 这是一个抽象类,我们并不直接使用它,而是使用它的两个子类——Sprite类和TiledLayer类
3> Sprite类
Sprite是精灵的意思,这是一个游戏开发的专有名词,在我们游戏中的每一个对象,我们都可以称为一个精灵。Sprite类提供了画面的翻转、旋转及简单的碰撞检测等。要在GameCanvas上绘制一个精灵对象也很简单,只需要调用Sprite的paint方法,该方法需要一个Graphics类型的参数,我们把缓冲区的Graphics对象实例传递给它即可。
4> TiledLayer类
Tile也是一个二维游戏开发的经典词汇,是砖块的意思。这个类有什么用呢?主要是用来构建地图。这个道理一想也很容易明白,我们玩的游戏中,地图往往比屏幕窗口大很多,难道我们需要做这么大的图片吗?当然不是,我们只需要做几个小图片,它这些图片按照一定的顺序平铺,就可以得到一个相当大的地图。TiledLayer类就为我们提供了这样的功能。
5> LayerManager类
这个类主要是用来管理Layer,它可以在画布上分层次的绘制精灵和地图,这样,就可以比较方便的解决谁在前、谁在后、谁遮挡谁等问题。
下面来看实例。刚才已经说过,游戏设计是一个很复杂的过程,没有专业的队伍是很难搞的。当然,我们也不是没有另类的搞法,那就是翻版。想一想我们从小时候到现在玩过哪些经典游戏?俄罗斯方块、超级玛丽、合金弹头、雷电、街霸、拳皇等等,还有前两年非常流行的“是男人系列”,这些游戏各有各的运行平台,后来又大部分移植到PC平台,现在,我们何不试试将它们移植到手机上呢?通过前面的介绍,不难看出,这些经典的2D游戏都可以使用上面的几个类来概括:飞机、子弹、敌人都是Sprite,大海、天空、森林、沙漠都是TileLayer,我们所面临的难题,就是搜集和制作图片素材而已。
这里的实例是“是男人就下一百层”的手机翻版,下载地址:
http://www.j2medev.com/Soft/src/game/200610/802.html,作者不详,反正不是我。下面是运行效果图:
首先,我们看一看程序的起点,也就是我们的MIDlet类,在这个程序的LRunner类的构在函数中,创建了一个gameCanv对象,如下:
public LRunner() {
dp=Display.getDisplay(this);
gc=new gameCanv();
dp.setCurrent(gc);
} 然后,在commandAction()函数中,这样启动游戏:
public void commandAction(Command c,Displayable d)
{
if(c==cmd_exit)
{
try
{
destroyApp(false);
}catch(Exception e){}
notifyDestroyed();
}
if((c==cmd_start)&(!gc.getIsOnGame()))
{
gc.reStart();
}
} 于是,游戏的主要逻辑全部交给gameCanv类了,这个类,就是我们前面讲到的GameCanvas类的子类,在这个类中,它实现了Runnable接口,并创建一个新的线程,以实现游戏的主循环,如下:
public void run() //主线程
{
isOnGame=true;
while(isOnGame)
{
gameCount++;
gameLevelUp(gameCount);
try
{
Thread.sleep(gameSpeed);
}catch(Exception e1){}
addGameObj();
for(int j=0;j<objList.size();j++) //游戏人物和台阶的处理
{
gfo=(gameFlatObj)objList.elementAt(j);
gfo.up();
if(gco==null)
{
gco=new gameCharObj(gfo.getX(),gfo.getY());
}
if(gfo.collidesWith(gco,false))
{
isOnFlat=true;
gfo.doOnChar(gco);
}
else
{
gfo.resetTime();
}
if(gfo.getProperties()>1)
{
gfo.nextFrame();
}
gfo.paint(g);
}
if(gfyo!=null)
{
if(gfyo.collidesWith(gco,false))
{
gfyo.doOnChar(gco);
gfyo=null;
}
else
{
gfyo.go();
gfyo.nextFrame();
gfyo.paint(g);
if(gfyo.getIsBottom())
{
gfyo=null;
}
}
}
keyPressed();
if(!isOnFlat)
{
gco.go();
}
isOnFlat=false;
//***************
if(gameBgCount<0)
{
gameBgCount++;
}
else
{
gameBgCount=-20;
}
//***************
gco.nextFrame();
gco.paint(g);
//***************
drawMenu(g);
//***************
g.drawImage(gameTeeth,0,20,0);
this.flushGraphics();
chkObjIsTop();
if(gco.isDead())
{
setOnGame(false);
}
}
this.flushGraphics();
} 从以上的代码中可以看出,这是一个典型的主循环,它通过调用addGameObj()来随机创建物体,而这些物体中,横板、翻板、传送带、钉板等物体作者将之称为gfo,对应的类为gameFlatObj,而从天而降的圆球称为gfyo,对应的类为gameFlyObj,而这几个类,当然是Sprite类的子类了。在这个主循环中,不难看到gfo.paint(g)和this.flushGraphics()这样的代码,正好和我们前面所说的GameCanvas类实现了双缓冲是对应的。
怎样轮询键盘输入呢?从上面的代码可以看出,作者将轮询键盘事件的代码放到了keyPressed()函数中,我们再来看看这个函数:
private void keyPressed()
{
int keyState=this.getKeyStates();
if((keyState&GameCanvas.LEFT_PRESSED)!=0)
{
if(gco!=null)
{
gco.setCharAct(0,isOnFlat);
gco.left(isOnFlat);
}
return;
}
if((keyState&GameCanvas.RIGHT_PRESSED)!=0)
{
if(gco!=null)
{
gco.setCharAct(1,isOnFlat);
gco.right(isOnFlat);
}
return;
}
if(gco!=null)
{
gco.setCharAct(2,isOnFlat);
}
} 和我们前面的介绍也是刚好一一对应。
下面我们再来看看Sprite类怎么使用。这里,gfo、gfyo等等精灵我就不讲了,只看看gco,这个精灵是我们游戏的主角,也就是那个跑来跑去跳上跳下的那个小人儿。该精灵对应的素材图片如下:
这个图片的文件名为char.png,不难看出,第一帧图象是站立不动时的效果,第2-5帧为向左跑动的效果,第6-9帧为向右跑动的效果,10-13帧为上下跳动的效果。Sprite类有一个构造函数Sprite(Image img,int x, int y),其中第一个参数就是该素材图片,后面的两个参数为每一帧图象的宽和高,至于总共有多少帧图象,Sprite类会自己计算。
gco精灵所对应的类为gameCharObj,从它的构造函数中,我们可以看出它正是以这个图片作为参数的,如下:
public class gameCharObj extends Sprite
{
static
{
try
{
img=Image.createImage("/char.png");
}
catch(Exception e){}
}
public gameCharObj(int X ,int Y)
{
super(img,16,17);
this.defineCollisionRectangle(1,16,15,1);
this.x=X;
this.y=Y-17;
this.setFrameSequence(stand);
this.setPosition(this.x,this.y);
}
} 在Sprite中,我们通过指定不同的帧序列,就可以实现动画,比如下面定义的四个数组,分别代表了站立时、向左跑时、向右跑时和跳起时的动画序列:
private int[] stand={0};
private int[] w_left={1,1,2,2,3,3,4,4};
private int[] w_right={5,5,6,6,7,7,8,8};
private int[] jump={9,9,10,10,11,11,12,12}; 当我们需要改变精灵的动画序列的时候,只需要调用Sprite.setFrameSequence()函数即可,如下面这段代码所示:
public void setCharAct(int i,boolean b) //改变人物绘图
{
switch(i)
{
case 0:
if(state!=i)
{
this.setFrameSequence(w_left);
state=i;
}
break;
case 1:
if(state!=i)
{
this.setFrameSequence(w_right);
state=i;
}
break;
case 2:
if((state!=i)||(face!=b))
{
this.setFrameSequence(b?stand:jump);
state=i;
face=b;
}
break;
}
} 设置完动画序列后,只有调用Sprite.nextFrame(),精灵才会真正的动起来,这一个调用是在主线程中完成的,大家回到开头就不难看到gco.nextFrame()这样的代码了。
遗憾的是,这个游戏中没有使用到TiledLayer和LayerManager这两个类,所以没有办法让大家看到实例了。当然,要想全面了解游戏开发的各个细节,最好还是找一些专业的书来读。最后,给大家介绍一个非常好的J2ME开发网站:
www.j2medev.com,这这里,大家可以找到很多文章和资源。